#!/usr/bin/env bash # install-service.sh # Создаёт systemd-сервис для запуска Minecraft сервера через ./run.sh # и сразу его поднимает. Должен быть запущен от root через sudo. # # Сервис будет работать от того же пользователя, который вызвал sudo # (т.е. от $SUDO_USER): не нужно создавать отдельного юзера, git pull и # редактирование конфигов работают как обычно. # # ВАЖНО: перед установкой сервиса один раз запустите сервер вручную: # ./start.sh # start.sh сам поставит Java (если не установлена), скачает и развернёт # NeoForge, сгенерирует user_jvm_args.txt, скачает server.jar и спросит # про EULA. После того как сервер успешно запустился (увидите 'Done!'), # остановите его (команда 'stop' в консоли или Ctrl+C) и запускайте # этот скрипт. Сервис будет использовать run.sh, который уже не делает # никаких интерактивных шагов — просто запускает java. # # Использование: # sudo ./install-service.sh # sudo ./install-service.sh --name minecraft # sudo ./install-service.sh --uninstall # # Флаги: # --name <имя> Имя systemd-сервиса (по умолчанию: minecraft) # --user Переопределить пользователя для сервиса # (по умолчанию: $SUDO_USER) # --dir <путь> Каталог сервера (по умолчанию: каталог скрипта) # --uninstall Остановить, выключить и удалить сервис # -h | --help Показать справку set -euo pipefail SERVICE_NAME="minecraft" SERVICE_USER="" SERVER_DIR="$(cd "$(dirname "$0")" && pwd)" UNINSTALL=0 print_help() { sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' } err() { echo "Ошибка: $*" >&2 exit 1 } while [[ $# -gt 0 ]]; do case "$1" in --name) SERVICE_NAME="$2"; shift 2 ;; --user) SERVICE_USER="$2"; shift 2 ;; --dir) SERVER_DIR="$2"; shift 2 ;; --uninstall) UNINSTALL=1; shift ;; -h|--help) print_help; exit 0 ;; *) err "Неизвестный аргумент: $1 (используйте --help)" ;; esac done # 1. Проверка прав root if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then err "скрипт должен быть запущен от root. Используйте: sudo $0 $*" fi # 2. Проверка systemd if ! command -v systemctl >/dev/null 2>&1; then err "systemctl не найден. Этот скрипт работает только на системах с systemd." fi UNIT_FILE="/etc/systemd/system/${SERVICE_NAME}.service" # Удаление сервиса if [[ "${UNINSTALL}" -eq 1 ]]; then echo ">> Останавливаю и удаляю сервис ${SERVICE_NAME}..." systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true systemctl disable "${SERVICE_NAME}.service" 2>/dev/null || true if [[ -f "${UNIT_FILE}" ]]; then rm -f "${UNIT_FILE}" echo " Удалён ${UNIT_FILE}" fi systemctl daemon-reload systemctl reset-failed "${SERVICE_NAME}.service" 2>/dev/null || true echo ">> Готово. Сервис ${SERVICE_NAME} удалён." exit 0 fi # 3. Определение пользователя сервиса if [[ -z "${SERVICE_USER}" ]]; then if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then SERVICE_USER="${SUDO_USER}" else err "не удалось определить пользователя. Запустите через sudo от обычного юзера, либо укажите --user <имя>." fi fi if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then err "пользователь '${SERVICE_USER}' не существует." fi SERVICE_GROUP="$(id -gn "${SERVICE_USER}")" SERVICE_HOME="$(getent passwd "${SERVICE_USER}" | cut -d: -f6)" echo ">> Сервис будет работать от: ${SERVICE_USER}:${SERVICE_GROUP}" # 3a. Поиск Java для systemd. systemd не загружает ~/.bashrc, поэтому # если Java стоит только через Jabba (~/.jabba/), unit её не увидит. # Проверяем по очереди: Jabba у пользователя сервиса -> системный PATH. JAVA_HOME_PATH="" JAVA_BIN_DIR="" if [[ -d "${SERVICE_HOME}/.jabba/jdk" ]]; then JABBA_JDK="$(ls -1 "${SERVICE_HOME}/.jabba/jdk/" 2>/dev/null | sort -V | tail -n1 || true)" if [[ -n "${JABBA_JDK}" ]] \ && [[ -x "${SERVICE_HOME}/.jabba/jdk/${JABBA_JDK}/bin/java" ]]; then JAVA_HOME_PATH="${SERVICE_HOME}/.jabba/jdk/${JABBA_JDK}" JAVA_BIN_DIR="${JAVA_HOME_PATH}/bin" echo ">> Найдена Java через Jabba: ${JAVA_BIN_DIR}/java" fi fi if [[ -z "${JAVA_BIN_DIR}" ]] && command -v java >/dev/null 2>&1; then SYSTEM_JAVA="$(command -v java)" echo ">> Найдена системная Java: ${SYSTEM_JAVA}" # Системную Java systemd увидит сам — Environment=PATH прописывать не нужно. fi if [[ -z "${JAVA_BIN_DIR}" ]] && ! command -v java >/dev/null 2>&1; then echo ">> ВНИМАНИЕ: java не найдена ни в PATH, ни в ~${SERVICE_USER}/.jabba." >&2 echo " Сервис, скорее всего, упадёт с 'java: command not found'." >&2 echo " Поставьте: sudo apt install -y openjdk-21-jre-headless" >&2 fi # 4. Проверка содержимого каталога сервера [[ -d "${SERVER_DIR}" ]] || err "каталог сервера не найден: ${SERVER_DIR}" [[ -f "${SERVER_DIR}/run.sh" ]] || err "не найден ${SERVER_DIR}/run.sh" chmod +x "${SERVER_DIR}/run.sh" 2>/dev/null || true # 5. Проверка готовности сервера (артефакты, которые создаёт start.sh) need_first_run() { cat >&2 </dev/null | head -n1 || true)" if [[ -z "${NEOFORGE_ARGS_FILE}" ]]; then need_first_run "не найден libraries/net/neoforged/neoforge/<версия>/unix_args.txt — NeoForge не развёрнут." fi NEOFORGE_ARGS_REL="${NEOFORGE_ARGS_FILE#${SERVER_DIR}/}" [[ -f "${SERVER_DIR}/user_jvm_args.txt" ]] \ || need_first_run "не найден user_jvm_args.txt — start.sh не сгенерировал JVM-аргументы." [[ -f "${SERVER_DIR}/server.jar" && -s "${SERVER_DIR}/server.jar" ]] \ || need_first_run "server.jar отсутствует или пустой — start.sh не докачал лаунчер." EULA_FILE="${SERVER_DIR}/eula.txt" if [[ ! -f "${EULA_FILE}" ]] || ! grep -qE '^\s*eula\s*=\s*true' "${EULA_FILE}"; then need_first_run "${EULA_FILE} отсутствует или eula=false — без принятия EULA сервер не стартует." fi # 5a. Генерируем launcher для systemd, который учитывает variables.txt. # run.sh не читает variables.txt и всегда использует системный `java`. SERVICE_LAUNCHER="${SERVER_DIR}/run-service.sh" cat > "${SERVICE_LAUNCHER}" <&2 return 0 fi local jargs="\${JAVA_ARGS:-}" jargs="\${jargs%\$'\r'}" if [[ -z "\${jargs// /}" ]]; then echo "\${_MC_LP} WARNING: JAVA_ARGS пуст в variables.txt — user_jvm_args.txt не перезаписан." >&2 return 0 fi cat > user_jvm_args.txt <<'JVMBLOCK' # Xmx and Xms set the maximum and minimum RAM usage, respectively. # They can take any number, followed by an M or a G. # M means Megabyte, G means Gigabyte. # For example, to set the maximum to 3GB: -Xmx3G # To set the minimum to 2.5GB: -Xms2500M # A good default for a modded server is 4GB. # Uncomment the next line to set it. # -Xmx4G JVMBLOCK printf '%s\n' "\${jargs}" >> user_jvm_args.txt echo "\${_MC_LP} user_jvm_args.txt обновлён из variables.txt (JAVA_ARGS)." } _regen_user_jvm_args echo "\${_MC_LP} ========== Minecraft server (systemd / run-service.sh) ==========" _JAVA_CMD="\${JAVA:-java}" if command -v "\$_JAVA_CMD" >/dev/null 2>&1; then echo "\${_MC_LP} Java: \$(command -v "\$_JAVA_CMD")" else echo "\${_MC_LP} Java: \$_JAVA_CMD" fi echo "\${_MC_LP} ADDITIONAL_ARGS: \${ADDITIONAL_ARGS:-}" echo "\${_MC_LP} JVM args (user_jvm_args.txt):" if [[ -f user_jvm_args.txt ]]; then grep -v '^[[:space:]]*#' user_jvm_args.txt | grep -v '^[[:space:]]*$' | sed "s/^/\${_MC_LP} /" || true else echo "\${_MC_LP} (user_jvm_args.txt not found)" fi echo "\${_MC_LP} Game/program args file: @\${GAME_ARGS_REL}" echo "\${_MC_LP} Trailing args: \$*" echo "\${_MC_LP} Java invocation pattern: \$_JAVA_CMD \${ADDITIONAL_ARGS:-} @user_jvm_args.txt @\${GAME_ARGS_REL} \$*" echo "\${_MC_LP} Логи — только блок запуска (grep):" echo "\${_MC_LP} sudo journalctl -u \${SERVICE_UNIT_NAME} -b --no-pager | grep -F '[mc-launch]'" echo "\${_MC_LP} Живой поток:" echo "\${_MC_LP} sudo journalctl -u \${SERVICE_UNIT_NAME} -f | grep --line-buffered -F '[mc-launch]'" echo "\${_MC_LP} ==============================================================" # shellcheck disable=SC2086 exec "\${JAVA:-java}" \${ADDITIONAL_ARGS:-} @user_jvm_args.txt @"${NEOFORGE_ARGS_REL}" "\$@" EOF chmod +x "${SERVICE_LAUNCHER}" # 6. Предупреждение про /home — типичная причина status=200/CHDIR. if [[ "${SERVER_DIR}" == /home/* ]]; then cat >&2 <> ВНИМАНИЕ: каталог сервера находится в /home/. Для systemd рекомендуется переносить сервисы в /opt (или /srv). ProtectHome будет ослаблен в unit-файле. EOF fi # 7. Генерация systemd unit-файла if [[ "${SERVER_DIR}" == /home/* ]]; then PROTECT_HOME_LINE="ProtectHome=false" else PROTECT_HOME_LINE="ProtectHome=read-only" fi echo ">> Создаю ${UNIT_FILE}..." cat > "${UNIT_FILE}" <> Перечитываю systemd, включаю и запускаю сервис..." systemctl daemon-reload systemctl enable "${SERVICE_NAME}.service" systemctl restart "${SERVICE_NAME}.service" sleep 2 echo "" echo "====================================================================" systemctl --no-pager --full status "${SERVICE_NAME}.service" || true echo "====================================================================" echo "" echo "Готово. Полезные команды:" echo " sudo systemctl status ${SERVICE_NAME}" echo " sudo systemctl stop ${SERVICE_NAME}" echo " sudo systemctl restart ${SERVICE_NAME}" echo " sudo journalctl -u ${SERVICE_NAME} -f # живые логи" echo " sudo journalctl -u ${SERVICE_NAME} -b --no-pager | grep -F '[mc-launch]' # JVM/запуск" echo " sudo journalctl -u ${SERVICE_NAME} -f | grep --line-buffered -F '[mc-launch]' # поток" echo " sudo $0 --uninstall # удалить сервис"