246 lines
9.9 KiB
Bash
246 lines
9.9 KiB
Bash
#!/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 <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}")"
|
||
echo ">> Сервис будет работать от: ${SERVICE_USER}:${SERVICE_GROUP}"
|
||
|
||
# 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 <<EOF
|
||
|
||
============================ ВНИМАНИЕ ============================
|
||
Сервер ещё не подготовлен к запуску через run.sh:
|
||
$1
|
||
|
||
run.sh — это лёгкий лаунчер, он только запускает java и не умеет
|
||
скачивать NeoForge / Java / server.jar и спрашивать EULA. Поэтому
|
||
ПЕРЕД установкой сервиса нужно один раз запустить сервер вручную:
|
||
|
||
cd ${SERVER_DIR}
|
||
./start.sh
|
||
|
||
start.sh сам:
|
||
- поставит Java (или подскажет, как поставить системно);
|
||
- скачает и развернёт NeoForge в libraries/;
|
||
- сгенерирует user_jvm_args.txt;
|
||
- скачает server.jar;
|
||
- спросит про EULA.
|
||
|
||
Когда сервер успешно поднимется (в логе появится 'Done (Xs)!'),
|
||
остановите его: в консоли наберите 'stop' и нажмите Enter
|
||
(или Ctrl+C). После этого запустите этот скрипт повторно:
|
||
|
||
sudo ./install-service.sh
|
||
====================================================================
|
||
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
# Файл NeoForge unix_args.txt — самый надёжный признак, что start.sh отработал.
|
||
NEOFORGE_ARGS_FILE="$(ls -1 "${SERVER_DIR}"/libraries/net/neoforged/neoforge/*/unix_args.txt 2>/dev/null | head -n1 || true)"
|
||
if [[ -z "${NEOFORGE_ARGS_FILE}" ]]; then
|
||
need_first_run "не найден libraries/net/neoforged/neoforge/<версия>/unix_args.txt — NeoForge не развёрнут."
|
||
fi
|
||
|
||
[[ -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
|
||
|
||
# 6. Проверка Java (run.sh вызывает 'java' из PATH)
|
||
if ! command -v java >/dev/null 2>&1; then
|
||
cat >&2 <<EOF
|
||
|
||
============================ ВНИМАНИЕ ============================
|
||
В системе не найдена команда 'java'. run.sh не сможет стартовать
|
||
сервер. Установите Java одним из способов:
|
||
|
||
# Системно (рекомендуется — видна и в shell, и в systemd):
|
||
sudo apt update
|
||
sudo apt install -y openjdk-21-jre-headless
|
||
|
||
После установки запустите этот скрипт повторно.
|
||
====================================================================
|
||
|
||
EOF
|
||
err "java не найдена в PATH."
|
||
fi
|
||
|
||
# 7. Предупреждение про /home — типичная причина status=200/CHDIR.
|
||
if [[ "${SERVER_DIR}" == /home/* ]]; then
|
||
cat >&2 <<EOF
|
||
|
||
>> ВНИМАНИЕ: каталог сервера находится в /home/. Для systemd
|
||
рекомендуется переносить сервисы в /opt (или /srv).
|
||
ProtectHome будет ослаблен в unit-файле.
|
||
|
||
EOF
|
||
fi
|
||
|
||
# 8. Генерация 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}" <<EOF
|
||
[Unit]
|
||
Description=Minecraft Server (${SERVICE_NAME})
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${SERVICE_USER}
|
||
Group=${SERVICE_GROUP}
|
||
WorkingDirectory=${SERVER_DIR}
|
||
ExecStart=/usr/bin/env bash ${SERVER_DIR}/run.sh nogui
|
||
Restart=on-failure
|
||
RestartSec=10s
|
||
TimeoutStopSec=120
|
||
KillSignal=SIGINT
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
# Безопасность
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=full
|
||
${PROTECT_HOME_LINE}
|
||
ReadWritePaths=${SERVER_DIR}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
chmod 644 "${UNIT_FILE}"
|
||
|
||
# 9. Активация сервиса
|
||
echo ">> Перечитываю 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 $0 --uninstall # удалить сервис"
|