feat: add bds core, locale updates and safety fixes

This commit is contained in:
2026-05-31 14:01:34 +03:00
parent d1686355e8
commit c769140913
34 changed files with 1535 additions and 12 deletions

View File

@@ -2,6 +2,19 @@
All notable changes to AstraDeploy are documented here. All notable changes to AstraDeploy are documented here.
## [Unreleased]
### Added
- Bedrock Dedicated Server (`bds`) core support:
- `--core bds`
- version-aware install via official Bedrock download source
- `.bds-version` marker and update flow support
- Additional UI polish for beginner mode with BDS version prompt.
### Fixed
- Improved `--force-clean` cleanup to clear hidden entries in server directories.
- Safer service unit generation for start-script paths containing special characters.
## [v0.1.0] - 2026-05-31 ## [v0.1.0] - 2026-05-31
### Added ### Added
@@ -39,4 +52,3 @@ All notable changes to AstraDeploy are documented here.
### Known limitations ### Known limitations
- Support matrix for OS/package-manager is intentionally MVP-focused (mainly Debian/Ubuntu family first, with extension points for others). - Support matrix for OS/package-manager is intentionally MVP-focused (mainly Debian/Ubuntu family first, with extension points for others).
- `--update` path is implemented per-core where available. - `--update` path is implemented per-core where available.

View File

@@ -46,6 +46,7 @@ Current version: `v0.1.0`.
| `velocity` | `/opt/minecraft/velocity` | `25577` | Java 21 | TCP | Requires Minecraft Java EULA acceptance | | `velocity` | `/opt/minecraft/velocity` | `25577` | Java 21 | TCP | Requires Minecraft Java EULA acceptance |
| `powernukkitx` | `/opt/minecraft/powernukkitx` | `19132` | Java 21 | UDP | Requires Minecraft Java EULA acceptance | | `powernukkitx` | `/opt/minecraft/powernukkitx` | `19132` | Java 21 | UDP | Requires Minecraft Java EULA acceptance |
| `nethergamesmc` | `/opt/minecraft/nethergamesmc` | `19132` | PHP | UDP | Prompts for PHP build and PocketMine-MCPE version | | `nethergamesmc` | `/opt/minecraft/nethergamesmc` | `19132` | PHP | UDP | Prompts for PHP build and PocketMine-MCPE version |
| `bds` | `/opt/minecraft/bds` | `19132` | native | UDP | Official Bedrock Dedicated Server zip binary |
> Service mode `screen` is available and handled in the service layer with the current installer version. > Service mode `screen` is available and handled in the service layer with the current installer version.
@@ -112,9 +113,10 @@ Run output is mirrored with `tee` and saved to:
### Core selection and versions ### Core selection and versions
- `--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc>` - `--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds>`
- `--mc-version <latest|version>` - `--mc-version <latest|version>`
- `--build <latest|tag>` (for `nethergamesmc`, PHP build) - `--build <latest|tag>` (for `nethergamesmc`, PHP build)
- `--mc-version` for `bds` resolves required Bedrock package version (`latest` allowed)
- legacy aliases: `--version`, `--core-version` for MC version - legacy aliases: `--version`, `--core-version` for MC version
### Directory and runtime ### Directory and runtime
@@ -167,6 +169,24 @@ sudo bash install.sh \
3. `PocketMine-MP` clone/build flow 3. `PocketMine-MP` clone/build flow
4. Server startup script generation 4. Server startup script generation
## Example for BDS
```bash
sudo bash install.sh \
--easy \
--core bds \
--dir /opt/minecraft/bds \
--mc-version latest \
--service tmux-systemd
```
`bds` flow includes:
1. resolve version from official Bedrock download page or provided tag;
2. download `bedrock-server-<version>.zip`;
3. extract/update server files;
4. generate `start.sh` and configure service.
## Update mode ## Update mode
`--update` switches workflow to update mode. The installer validates existing directory first and replaces server artifacts via the core-specific update path where supported. `--update` switches workflow to update mode. The installer validates existing directory first and replaces server artifacts via the core-specific update path where supported.

View File

@@ -46,6 +46,7 @@
| `velocity` | `/opt/minecraft/velocity` | `25577` | Java 21 | TCP | Нужна явная обработка/подтверждение EULA Java | | `velocity` | `/opt/minecraft/velocity` | `25577` | Java 21 | TCP | Нужна явная обработка/подтверждение EULA Java |
| `powernukkitx` | `/opt/minecraft/powernukkitx` | `19132` | Java 21 | UDP | Нужна явная обработка/подтверждение EULA Java | | `powernukkitx` | `/opt/minecraft/powernukkitx` | `19132` | Java 21 | UDP | Нужна явная обработка/подтверждение EULA Java |
| `nethergamesmc` | `/opt/minecraft/nethergamesmc` | `19132` | PHP | UDP | Доп. вопросы по PHP build и версии PocketMine-MCPE | | `nethergamesmc` | `/opt/minecraft/nethergamesmc` | `19132` | PHP | UDP | Доп. вопросы по PHP build и версии PocketMine-MCPE |
| `bds` | `/opt/minecraft/bds` | `19132` | native | UDP | Официальный бинарник Bedrock Dedicated Server |
> Режим `screen` доступен в текущей версии и настраивается через слой сервисов. > Режим `screen` доступен в текущей версии и настраивается через слой сервисов.
@@ -112,9 +113,12 @@ sudo bash install.sh \
### Ядро и версии ### Ядро и версии
- `--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc>` - `--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds>`
- `--mc-version <latest|version>` - `--mc-version <latest|version>`
- `--build <latest|tag>` (для `nethergamesmc`, версия PHP) - `--build <latest|tag>` (для `nethergamesmc`, версия PHP)
- `--mc-version` для `bds` и `nethergamesmc`:
- `nethergamesmc`: версия/тег PocketMine-MCPE
- `bds`: версия Bedrock Dedicated Server (`latest` определяется автоматически)
- legacy алиасы: `--version`, `--core-version` - legacy алиасы: `--version`, `--core-version`
### Каталог и рантайм ### Каталог и рантайм
@@ -167,6 +171,24 @@ sudo bash install.sh \
3. клонирование `PocketMine-MP`; 3. клонирование `PocketMine-MP`;
4. сборка/подготовка запуска. 4. сборка/подготовка запуска.
## Пример для BDS
```bash
sudo bash install.sh \
--easy \
--core bds \
--dir /opt/minecraft/bds \
--mc-version latest \
--service tmux-systemd
```
Путь для `bds`:
1. разрешение версии Bedrock;
2. скачивание `bedrock-server-<версия>.zip`;
3. разархивация и обновление файлов сервера;
4. генерация `start.sh` и настройка сервиса.
## Режим обновления ## Режим обновления
`--update` переводит в режим обновления. Сначала проверяется существование и непустота директории, затем выполняется core-специфичный апдейт. `--update` переводит в режим обновления. Сначала проверяется существование и непустота директории, затем выполняется core-специфичный апдейт.

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

291
bot-integration-fsm-plan.md Normal file
View File

@@ -0,0 +1,291 @@
# План интеграции AstraDeploy и бота (FSM + callback API)
## Цели
Сделать наблюдаемую, устойчивую и воспроизводимую установку через бота с поэтапными статусами.
Скрипт уже умеет отдавать:
- `started`
- `step`
- `success`
- `failed`
Нужно оборачивать это в `FSM`-движок на бэкенде бота.
## 1. Точка входа callback
### Endpoint
- `POST /astra-deploy/report`
- Заголовок: `Authorization: Bearer <report-token>`
- Content-Type: `application/json`
### Тело (из скрипта)
```json
{
"status": "step",
"core": "paper",
"dir": "/opt/minecraft/paper",
"port": 25565,
"protocol": "tcp",
"service": "tmux-systemd",
"message": "Installing base packages",
"step": "preflight_packages",
"step_index": 14,
"step_total": 80,
"step_percent": 17
}
```
### Рекомендуемый расширенный payload (бот-раннер может добавить)
```json
{
"request_id": "uuid",
"job_id": "job_123",
"user_id": "123",
"locale": "ru"
}
```
### Ответ
- `200/204` с `{ "ok": true }` или пустым телом.
- Идемпотентность по `(request_id, status, step, step_index, step_total, ts_sec)` или аналогичной паре.
## 2. Запуск задачи из бота
Ключевые параметры запускаемого процесса:
- `--core ...`
- `--dir ...`
- `--port ...`
- `--service systemd|tmux-systemd|screen|none`
- `--user ...`
- `--firewall yes|no`
- `--yes`
- `--accept-license yes` (обязательно в автоматическом)
- `--accept-eula yes` для Java-ядер
- `--report-url ...`
- `--report-token ...`
- `--locale ru|en` (или `LANGUAGE`/`LANG`)
- `--dry-run`, `--update`, `--force-clean`, `--backup-existing`, `--allow-existing`
- `--build`, `--mc-version` для `nethergamesmc`
## 3. FSM состояния
### Состояния
- `QUEUED`
- `RUNNING`
- `VALIDATION`
- `PREPARE_ENV`
- `CORE_INSTALL`
- `POST_INSTALL`
- `WAITING_INPUT`
- `ROLLBACK`
- `DONE`
- `FAILED`
- `DRY_RUN`
- `STALLED` (таймаут между шагами)
### Переходы
- `started` -> `RUNNING`
- `step(parse_complete|prepare_defaults|preflight_*)` -> `VALIDATION`
- `step(preflight_packages|preflight_java|preflight_user|preflight_directory)` -> `PREPARE_ENV`
- `step(install_core|deploy_core|update_core|core_*)` -> `CORE_INSTALL`
- `step(core_post|core_service|service_*|finalize)` -> `POST_INSTALL`
- `status=success` -> `DONE`
- `status=failed` -> `FAILED`
- `--dry-run` (локально определено) -> `DRY_RUN`
- долгий таймаут без новых step -> `STALLED`
- interactive без автоматических флагов и требующие ответа -> `WAITING_INPUT`
### Идёмпотентная обработка
- Игнорировать дубликаты последнего шага в том же `request_id`.
- Обновлять состояние только если новый `step_index` выше предыдущего или статус изменился на более поздний.
## 4. Полная карта `step`
### Общая фаза и предзапуск
- `parse_complete`
- `prepare_defaults`
- `preflight_start`
- `install_start`
- `finalize`
- `core_runtime`
- `core_install`
- `core_update`
### Предпроверки
- `preflight_begin`
- `preflight_validate_root`
- `preflight_detect`
- `preflight_detect_os`
- `preflight_validate_support`
- `preflight_update_mode_check`
- `preflight_validate_plan`
- `preflight_validate_licenses`
- `preflight_validate_port`
- `preflight_directory`
- `preflight_packages`
- `preflight_java`
- `preflight_user`
### Установка/обновление ядер
- `core_resolve_channel`
- `core_resolve_download`
- `core_fetch`
- `core_render`
- `core_configure`
- `core_post`
- `core_service`
- `core_chown`
- `core_backup`
- `core_update_done`
- `core_phpbuild_resolve`
- `core_phpbuild_fetch`
- `core_composer`
- `core_repo_clone`
- `core_install`
- `install_core`
- `deploy_core`
- `update_core`
### Системные шаги
- `packages_start`
- `packages_dry_run`
- `packages_index`
- `download_start`
- `download_completed`
- `download_swap`
- `firewall_start`
- `firewall_done`
- `firewall_manual`
- `java_install`
- `render_start_script`
- `chmod_start_script`
- `service_stop`
- `service_create`
- `service_reload_enable`
- `service_start`
- `user_create`
- `user_exists`
- `prepare_dir`
- `permission_fix`
## 5. Маппинг на UX-сообщения (ru/en)
### RU
- `preflight_*` → «Подготовка окружения и проверок»
- `preflight_packages` / `packages_*` → «Установка базовых пакетов»
- `core_*` → «Установка и настройка ядра»
- `service_*`, `core_service` → «Настройка автозапуска»
- `core_chown` / `prepare_dir` / `permission_fix` → «Настройка доступа»
- `success` → «Установка завершена»
- `failed` → «Ошибка установки»
### EN
- `preflight_*``Environment preflight`
- `core_*``Core installation`
- `service_*``Service setup`
- `failed``Deployment failed`
## 6. Edge-cases по сценариям запуска
- **Interactive (без `--yes`)**
- может остановиться на подтверждении лицензии/ЕULA/директории
- статус `WAITING_INPUT`
- **`--easy`**
- обычный процесс, добавляется план-сводка до preflight
- **`--update`**
- `step_total` уменьшается на 6, проверяется существующая непустая директория
- **`--dry-run`**
- физически без изменений, callback может отсутствовать (как реализовано сейчас)
- **`--force-clean`**
- удаление содержимого директории перед установкой
- **`--backup-existing`**
- резервная копия `*.backup-<timestamp>`
- **`--allow-existing`**
- пропуск проверки непустой директории
- **Неправильный `core` / OS**
- жёсткий fail с объяснением
- **Порт занят**
- fail с деталями `tcp/udp` и предложением сменить порт
- **Сбой callback API**
- для скрипта не фатальный; бот не должен переводить задачу в `FAILED` из-за 5xx webhook-а
## 7. Rollback и пост-факту
На фатальной ошибке скрипт делает:
- лог ошибки
- попытку удаления созданного сервиса (если был создан)
- сообщение с путём лога (`/var/log/astra-deploy/...`)
- завершение с кодом ошибки
Для бота:
- статусы -> `ROLLBACK` (внутренний подстейт)
- показать пользователю:
- возможная причина
- путь лога
- безопасный список ручных действий
## 8. Минимальная модель хранения
### `jobs`
- `id`
- `user_id`
- `core`, `dir`, `port`, `service`, `locale`
- `fsm_state`
- `status`
- `percent`
- `step`, `step_index`, `step_total`, `message`
- `request_id`, `report_token_id`, `process_pid`
- `created_at`, `updated_at`
### `job_events`
- `job_id`
- `status`, `step`, `step_index`, `step_total`, `step_percent`
- `message`
- `raw_payload` (json)
- `created_at`
## 9. Шаблоны уведомлений
### Telegram (RU)
1) Запуск: `🚀 Установка AstraDeploy запущена`
2) Шаг: `⚙️ {percent}% | {state} | {step}`
3) Успех: `✅ Установка завершена`
4) Ошибка: `❌ Установка завершена с ошибкой`
### Telegram (EN)
1) `🚀 AstraDeploy started`
2) `⚙️ {percent}% | {state} | {step}`
3) `✅ Installation completed`
4) `❌ Installation failed`
Кнопки:
- `Получить статус`
- `Логи`
- `Запустить проверку`
- `Повторить (dry-run)`
## 10. Безопасность
- Токены webhook не логировать.
- Проверять `Authorization` строго.
- Проверка whitelist по разрешённым `step`.
- Rate limit на report endpoint.
- Защищённое хранение токенов (vault/secret/env).
## 11. Проверочный набор для интеграции
- `paper` non-interactive, валидные флаги
- `paper` без `--accept-eula` и с `--yes` -> `failed`
- `nethergamesmc --dry-run` -> без изменений, шаги в локальном логе
- занятый порт -> `failed`
- `--update` на пустой директории -> `failed`
- `--force-clean` на непустой директории -> success после очистки
- callback 5xx -> установка продолжается, без ложного FAILED в UI
## 12. Что отдаёт разработчику бота на старте
- список поддерживаемых `core`: `pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc`
- поддерживаемые режимы сервиса: `systemd`, `tmux-systemd`, `screen`, `none`
- список ключевых шагов из этого плана выше
- статусные правила и fallback-поведение для отсутствующего callback

152
cores/bds.sh Normal file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env bash
core_name_bds() {
echo "Bedrock Dedicated Server"
}
core_default_port_bds() {
echo "19132"
}
core_protocol_bds() {
echo "udp"
}
core_runtime_bds() {
echo "native"
}
core_validate_bds() {
if [[ -z "${MC_VERSION:-}" ]]; then
MC_VERSION="latest"
fi
}
bds_resolve_latest_download_url() {
local page
local url
page="$(http_get "https://www.minecraft.net/en-us/download/server/bedrock")"
if [[ -z "$page" ]]; then
fail "$(t no_stable_build "Bedrock Dedicated Server" "latest")"
fi
url="$(printf '%s\n' "$page" | grep -oE "https://minecraft\\.azureedge\\.net/bin-linux/bedrock-server-[^\" ]+\\.zip" | head -n 1)"
if [[ -z "$url" ]]; then
fail "$(t no_stable_build "Bedrock Dedicated Server" "latest")"
fi
echo "$url"
}
bds_resolve_download_url() {
local requested="${1:-latest}"
if [[ "$requested" == "latest" ]]; then
bds_resolve_latest_download_url
return
fi
if [[ "$requested" == http* ]]; then
echo "$requested"
return
fi
echo "https://minecraft.azureedge.net/bin-linux/bedrock-server-${requested}.zip"
}
bds_resolve_version_from_url() {
local url="$1"
local requested="${2:-latest}"
local file
file="${url##*/}"
file="${file#bedrock-server-}"
file="${file%.zip*}"
file="${file%%\?*}"
if [[ -z "$file" || "$file" == "$requested" ]]; then
echo "$requested"
return
fi
echo "$file"
}
bds_render_start_script() {
local dir="$SERVER_DIR"
emit_step "core_render" "bds start script"
run_cmd bash -c "cat > '$dir/start.sh' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
export LD_LIBRARY_PATH=.:\$LD_LIBRARY_PATH
exec ./bedrock_server
EOF"
run_cmd chmod +x "$dir/start.sh"
emit_step "chmod_start_script" "$dir/start.sh"
}
core_install_bds() {
info "$(t core_install "$(core_name_bds)")"
emit_step "core_install" "$(core_name_bds)"
local requested_version="${MC_VERSION:-latest}"
local download_url
local version
local archive="$SERVER_DIR/.bds-package.zip"
download_url="$(bds_resolve_download_url "$requested_version")"
version="$(bds_resolve_version_from_url "$download_url" "$requested_version")"
emit_step "core_resolve_download" "$version"
emit_step "core_fetch" "$download_url"
if [[ -f "$SERVER_DIR/.bds-version" ]]; then
local existing_version
existing_version="$(cat "$SERVER_DIR/.bds-version" 2>/dev/null || true)"
if [[ "$UPDATE_MODE" == "no" && -n "$existing_version" && "$existing_version" == "$version" && -f "$SERVER_DIR/bedrock_server" ]]; then
info "BDS already at requested version ${version}. Skipping binary download."
run_cmd chmod +x "$SERVER_DIR/bedrock_server"
bds_render_start_script
return
fi
fi
run_cmd rm -f "$archive"
download_as_user "$download_url" "$archive"
emit_step "core_fetch" "Bedrock server archive"
run_cmd unzip -o -q "$archive" -d "$SERVER_DIR"
run_cmd rm -f "$archive"
if [[ ! -f "$SERVER_DIR/bedrock_server" ]]; then
fail "bedrock_server not found after extraction"
fi
run_cmd chmod +x "$SERVER_DIR/bedrock_server"
bds_render_start_script
run_cmd bash -lc "printf '%s\n' '$version' > '$SERVER_DIR/.bds-version'"
emit_step "core_configure" "BDS version ${version}"
}
core_post_install_bds() {
emit_step "core_post" "BDS"
run_cmd bash -lc "chown -R ${SERVER_USER}:${SERVER_USER} '$SERVER_DIR'"
emit_step "core_chown" "BDS"
open_firewall_port "$SERVER_PORT" "udp"
emit_step "core_service" "BDS"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_bds() {
info "$(t core_update "$(core_name_bds)")"
emit_step "core_update" "$(core_name_bds)"
backup_file "$SERVER_DIR/bedrock_server"
backup_file "$SERVER_DIR/start.sh"
backup_file "$SERVER_DIR/.bds-version"
core_install_bds
emit_step "core_update_done" "BDS"
}

59
cores/paper.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
core_name_paper() {
echo "Paper"
}
core_default_port_paper() {
echo "25565"
}
core_protocol_paper() {
echo "tcp"
}
core_runtime_paper() {
echo "java"
}
core_validate_paper() {
return 0
}
core_install_paper() {
info "$(t core_install "$(core_name_paper)")"
emit_step "core_install" "$(core_name_paper)"
local url
emit_step "core_resolve_download" "paper"
url="$(get_papermc_download_url "paper" "${MC_VERSION:-latest}")"
download_as_user "$url" "$SERVER_DIR/server.jar"
emit_step "core_configure" "eula"
run_cmd bash -lc "printf 'eula=true\n' > '$SERVER_DIR/eula.txt'"
create_java_start_script "$SERVER_DIR" "server.jar" "$RAM"
}
core_post_install_paper() {
emit_step "core_post" "Paper"
run_cmd bash -lc "chown -R ${SERVER_USER}:${SERVER_USER} '$SERVER_DIR'"
emit_step "core_chown" "Paper"
open_firewall_port "$SERVER_PORT" "tcp"
emit_step "core_service" "Paper"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_paper() {
info "$(t core_update "$(core_name_paper)")"
emit_step "core_update" "$(core_name_paper)"
local target="$SERVER_DIR/server.jar"
backup_file "$target"
emit_step "core_backup" "$target"
local url
emit_step "core_resolve_download" "paper"
url="$(get_papermc_download_url "paper" "${MC_VERSION:-latest}")"
download_as_user_atomic "$url" "$target"
emit_step "core_update_done" "Paper"
}

62
cores/pmmp.sh Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
core_name_pmmp() {
echo "PocketMine-MP"
}
core_default_port_pmmp() {
echo "19132"
}
core_protocol_pmmp() {
echo "udp"
}
core_runtime_pmmp() {
echo "php"
}
core_validate_pmmp() {
return 0
}
core_install_pmmp() {
info "$(t core_install "$(core_name_pmmp)")"
emit_step "core_install" "$(core_name_pmmp)"
emit_step "core_resolve_channel" "${BUILD:-latest}"
local channel="${BUILD}"
if [[ "$channel" == "latest" || -z "$channel" ]]; then
channel="stable"
fi
download_url="https://get.pmmp.io"
emit_step "core_fetch" "pmmp"
run_cmd sudo -u "$SERVER_USER" bash -lc "cd '$SERVER_DIR' && curl -sL '$download_url' | bash -s - -d '$SERVER_DIR' -v '$channel'"
emit_step "core_configure" "pmmp"
}
core_post_install_pmmp() {
emit_step "core_post" "pmmp"
open_firewall_port "$SERVER_PORT" "udp"
emit_step "core_service" "pmmp"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_pmmp() {
info "$(t core_update "$(core_name_pmmp)")"
emit_step "core_update" "$(core_name_pmmp)"
local download_url="https://get.pmmp.io"
local channel="${BUILD}"
if [[ "$channel" == "latest" || -z "$channel" ]]; then
channel="stable"
fi
backup_file "$SERVER_DIR/server.phar"
backup_file "$SERVER_DIR/start.sh"
emit_step "core_fetch" "pmmp"
run_cmd sudo -u "$SERVER_USER" bash -lc "cd '$SERVER_DIR' && curl -sL '$download_url' | bash -s - -d '$SERVER_DIR' -v '$channel'"
emit_step "core_update_done" "pmmp"
}

65
cores/powernukkitx.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
core_name_powernukkitx() {
echo "PowerNukkitX"
}
core_default_port_powernukkitx() {
echo "19132"
}
core_protocol_powernukkitx() {
echo "udp"
}
core_runtime_powernukkitx() {
echo "java"
}
core_validate_powernukkitx() {
return 0
}
core_install_powernukkitx() {
info "$(t core_install "$(core_name_powernukkitx)")"
emit_step "core_install" "$(core_name_powernukkitx)"
local url
emit_step "core_resolve_download" "powernukkitx"
url="$(get_github_release_asset_url PowerNukkitX/PowerNukkitX 'powernukkitx.*\\.jar')"
if [[ -z "$url" || "$url" == "null" ]]; then
fail "$(t no_stable_build "PowerNukkitX" "$BUILD")"
fi
download_as_user "$url" "$SERVER_DIR/powernukkitx.jar"
create_powernukkit_start_script "$SERVER_DIR" "$RAM"
emit_step "core_render" "powernukkitx"
}
core_post_install_powernukkitx() {
emit_step "core_configure" "eula"
run_cmd bash -lc "printf 'eula=true\n' > '$SERVER_DIR/eula.txt'"
emit_step "core_chown" "PowerNukkitX"
run_cmd bash -lc "chown -R ${SERVER_USER}:${SERVER_USER} '$SERVER_DIR'"
open_firewall_port "$SERVER_PORT" "udp"
emit_step "core_service" "PowerNukkitX"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_powernukkitx() {
info "$(t core_update "$(core_name_powernukkitx)")"
emit_step "core_update" "$(core_name_powernukkitx)"
local target="$SERVER_DIR/powernukkitx.jar"
backup_file "$target"
emit_step "core_backup" "$target"
local url
emit_step "core_resolve_download" "powernukkitx"
url="$(get_github_release_asset_url PowerNukkitX/PowerNukkitX 'powernukkitx.*\\.jar')"
if [[ -z "$url" || "$url" == "null" ]]; then
fail "$(t no_stable_build "PowerNukkitX" "$BUILD")"
fi
download_as_user_atomic "$url" "$target"
emit_step "core_update_done" "PowerNukkitX"
}

59
cores/purpur.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
core_name_purpur() {
echo "Purpur"
}
core_default_port_purpur() {
echo "25565"
}
core_protocol_purpur() {
echo "tcp"
}
core_runtime_purpur() {
echo "java"
}
core_validate_purpur() {
return 0
}
core_install_purpur() {
info "$(t core_install "$(core_name_purpur)")"
emit_step "core_install" "$(core_name_purpur)"
local url
emit_step "core_resolve_download" "purpur"
url="$(get_purpur_download_url "${MC_VERSION:-latest}" "${BUILD:-latest}")"
download_as_user "$url" "$SERVER_DIR/server.jar"
emit_step "core_configure" "eula"
run_cmd bash -lc "printf 'eula=true\n' > '$SERVER_DIR/eula.txt'"
create_java_start_script "$SERVER_DIR" "server.jar" "$RAM"
}
core_post_install_purpur() {
emit_step "core_post" "Purpur"
run_cmd bash -lc "chown -R ${SERVER_USER}:${SERVER_USER} '$SERVER_DIR'"
emit_step "core_chown" "Purpur"
open_firewall_port "$SERVER_PORT" "tcp"
emit_step "core_service" "Purpur"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_purpur() {
info "$(t core_update "$(core_name_purpur)")"
emit_step "core_update" "$(core_name_purpur)"
local target="$SERVER_DIR/server.jar"
backup_file "$target"
emit_step "core_backup" "$target"
local url
emit_step "core_resolve_download" "purpur"
url="$(get_purpur_download_url "${MC_VERSION:-latest}" "${BUILD:-latest}")"
download_as_user_atomic "$url" "$target"
emit_step "core_update_done" "Purpur"
}

65
cores/velocity.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
core_name_velocity() {
echo "Velocity"
}
core_default_port_velocity() {
echo "25577"
}
core_protocol_velocity() {
echo "tcp"
}
core_runtime_velocity() {
echo "java"
}
core_validate_velocity() {
return 0
}
core_install_velocity() {
info "$(t core_install "$(core_name_velocity)")"
emit_step "core_install" "$(core_name_velocity)"
local url
emit_step "core_resolve_download" "velocity"
url="$(get_papermc_download_url "velocity" "${MC_VERSION:-latest}")"
download_as_user "$url" "$SERVER_DIR/velocity.jar"
run_cmd bash -c "cat > '$SERVER_DIR/start.sh' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
exec java -Xms${RAM} -Xmx${RAM} -jar velocity.jar
EOF"
run_cmd chmod +x "$SERVER_DIR/start.sh"
emit_step "chmod_start_script" "$SERVER_DIR/start.sh"
emit_step "core_render" "velocity"
}
core_post_install_velocity() {
emit_step "core_post" "Velocity"
run_cmd bash -lc "chown -R ${SERVER_USER}:${SERVER_USER} '$SERVER_DIR'"
emit_step "core_chown" "Velocity"
open_firewall_port "$SERVER_PORT" "tcp"
emit_step "core_service" "Velocity"
create_service "$CORE" "$SERVER_DIR" "$SERVICE_MODE" "$SERVER_DIR/start.sh"
start_service
}
core_update_velocity() {
info "$(t core_update "$(core_name_velocity)")"
emit_step "core_update" "$(core_name_velocity)"
local target="$SERVER_DIR/velocity.jar"
backup_file "$target"
emit_step "core_backup" "$target"
local url
emit_step "core_resolve_download" "velocity"
url="$(get_papermc_download_url "velocity" "${MC_VERSION:-latest}")"
download_as_user_atomic "$url" "$target"
emit_step "core_update_done" "Velocity"
}

View File

@@ -196,6 +196,32 @@ You can use script output or report callback events:
- `success` - `success`
- `failed` - `failed`
### BDS (`--core bds`)
- Beginner:
```bash
sudo bash install.sh --easy --core bds --service tmux-systemd
```
- Non-interactive:
```bash
sudo bash install.sh \
--yes \
--core bds \
--dir /opt/minecraft/bds \
--service systemd \
--mc-version latest \
--accept-license yes \
--firewall yes \
--locale en
```
Notes:
- `--mc-version` for BDS means Bedrock server version (example: `1.21.3.01`) or `latest`.
## Update mode ## Update mode
Use for replacing files on an existing server directory: Use for replacing files on an existing server directory:
@@ -210,4 +236,3 @@ sudo bash install.sh \
``` ```
For Java cores add `--accept-eula yes`. For Java cores add `--accept-eula yes`.

View File

@@ -196,6 +196,32 @@
- `success` - `success`
- `failed` - `failed`
### BDS (`--core bds`)
- Easy:
```bash
sudo bash install.sh --easy --core bds --service tmux-systemd
```
- Non-interactive:
```bash
sudo bash install.sh \
--yes \
--core bds \
--dir /opt/minecraft/bds \
--service systemd \
--mc-version latest \
--accept-license yes \
--firewall yes \
--locale ru
```
Комментарии:
- `--mc-version` для `bds` — это версия Bedrock сервера (`latest` или конкретный номер версии, например `1.21.3.01`).
## Режим обновления ## Режим обновления
Для обновления уже установленного каталога: Для обновления уже установленного каталога:
@@ -210,4 +236,3 @@ sudo bash install.sh \
``` ```
Для Java-ядер добавляйте `--accept-eula yes`. Для Java-ядер добавляйте `--accept-eula yes`.

View File

@@ -69,7 +69,7 @@ Usage:
curl -fsSL https://deploy.example.com/install.sh | sudo bash -s -- [options] curl -fsSL https://deploy.example.com/install.sh | sudo bash -s -- [options]
Options: Options:
--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc> --core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds>
--core-version <latest|version> (legacy alias for mc-version) --core-version <latest|version> (legacy alias for mc-version)
--dir <path> --dir <path>
--port <number> --port <number>
@@ -313,7 +313,7 @@ resolve_core_defaults() {
local check_fn="core_default_port_${CORE}" local check_fn="core_default_port_${CORE}"
if [[ "$(type -t "$check_fn" 2>/dev/null)" != "function" ]]; then if [[ "$(type -t "$check_fn" 2>/dev/null)" != "function" ]]; then
fail "$(t unsupported_core "$CORE" "pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc" "${SCRIPT_DIR}/manifests/cores.json")" fail "$(t unsupported_core "$CORE" "pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc,bds" "${SCRIPT_DIR}/manifests/cores.json")"
fi fi
CORE_NAME="$(core_name_${CORE})" CORE_NAME="$(core_name_${CORE})"
@@ -423,6 +423,8 @@ show_noob_plan() {
if [[ "$CORE" == "nethergamesmc" ]]; then if [[ "$CORE" == "nethergamesmc" ]]; then
info "$(t easy_plan_build "$BUILD")" info "$(t easy_plan_build "$BUILD")"
info "$(t easy_plan_mc_version "$MC_VERSION")" info "$(t easy_plan_mc_version "$MC_VERSION")"
elif [[ "$CORE" == "bds" ]]; then
info "$(t easy_plan_bds_version "$MC_VERSION")"
fi fi
if is_java_core "$CORE"; then if is_java_core "$CORE"; then
info "$(t easy_plan_eula "$ACCEPT_EULA")" info "$(t easy_plan_eula "$ACCEPT_EULA")"
@@ -452,6 +454,8 @@ interact() {
if [[ "$CORE" == "nethergamesmc" ]]; then if [[ "$CORE" == "nethergamesmc" ]]; then
ask_value "$(t prompt_php_build)" "${BUILD:-latest}" BUILD ask_value "$(t prompt_php_build)" "${BUILD:-latest}" BUILD
ask_value "$(t prompt_pocketmine_version)" "${MC_VERSION:-latest}" MC_VERSION ask_value "$(t prompt_pocketmine_version)" "${MC_VERSION:-latest}" MC_VERSION
elif [[ "$CORE" == "bds" ]]; then
ask_value "$(t prompt_bds_version)" "${MC_VERSION:-latest}" MC_VERSION
fi fi
ask_value "$(t prompt_service_mode)" "$SERVICE_MODE" SERVICE_MODE ask_value "$(t prompt_service_mode)" "$SERVICE_MODE" SERVICE_MODE
ask_value "$(t prompt_open_firewall)" "$FIREWALL" FIREWALL ask_value "$(t prompt_open_firewall)" "$FIREWALL" FIREWALL
@@ -525,7 +529,7 @@ check_existing_directory() {
fi fi
if [[ "$FORCE_CLEAN" == "yes" ]]; then if [[ "$FORCE_CLEAN" == "yes" ]]; then
run_cmd find "$SERVER_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + run_cmd bash -c "find \"$SERVER_DIR\" -mindepth 1 -delete"
return return
fi fi

33
lib/detect.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
detect_system() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
OS_NAME="${PRETTY_NAME:-unknown}"
else
fail "$(t detect_os_failed)"
fi
ARCHITECTURE="$(uname -m)"
if command -v apt-get >/dev/null 2>&1; then
PKG_MANAGER="apt"
elif command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
elif command -v pacman >/dev/null 2>&1; then
PKG_MANAGER="pacman"
else
fail "$(t no_package_manager)"
fi
if command -v systemctl >/dev/null 2>&1; then
INIT_SYSTEM="systemd"
else
INIT_SYSTEM="none"
fi
}

65
lib/download.sh Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
http_get() {
local url="$1"
curl -fsSL -H "User-Agent: AstraDeploy/${DEPLOY_VERSION}" "$url"
}
get_papermc_download_url() {
local project="$1"
local version="$2"
if [[ "$version" == "latest" ]]; then
version="$(http_get "https://fill.papermc.io/v3/projects/${project}" | jq -r '.versions | if type=="array" then .[-1] else (to_entries | last | .value[0]) end')"
fi
local builds_json
builds_json="$(http_get "https://fill.papermc.io/v3/projects/${project}/versions/${version}/builds")"
local url
url="$(printf '%s\n' "$builds_json" | jq -r '.builds | map(select(.channel=="STABLE")) | .[0].downloads."server:default".url // empty')"
if [[ -z "$url" || "$url" == "null" ]]; then
fail "$(t no_stable_build "$project" "$version")"
fi
echo "$url"
}
get_purpur_download_url() {
local version="$1"
local build="${2:-latest}"
if [[ "$version" == "latest" ]]; then
version="$(http_get "https://api.purpurmc.org/v2/purpur/" | jq -r '.versions | .[-1]')"
fi
if [[ "$build" == "latest" ]]; then
echo "https://api.purpurmc.org/v2/purpur/${version}/latest/download"
else
echo "https://api.purpurmc.org/v2/purpur/${version}/${build}/download"
fi
}
get_github_release_asset_url() {
local repo="$1"
local pattern="$2"
http_get "https://api.github.com/repos/${repo}/releases/latest" \
| jq -r --arg pattern "$pattern" '.assets[] | select(.name | test($pattern)) | .browser_download_url' | head -n 1
}
download_as_user() {
local url="$1"
local target="$2"
emit_step "download_start" "$url"
run_cmd sudo -u "$SERVER_USER" bash -lc "curl -fL \"$url\" -o \"${target}\""
emit_step "download_completed" "$target"
}
download_as_user_atomic() {
local url="$1"
local target="$2"
local tmp="${target}.tmp.${RANDOM}.$(date +%s)"
download_as_user "$url" "$tmp"
run_cmd mv "$tmp" "$target"
emit_step "download_swap" "$target"
}

30
lib/firewall.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
open_firewall_port() {
local port="$1"
local proto="$2"
if [[ "$FIREWALL" != "yes" ]]; then
info "$(t firewall_skip)"
return 0
fi
emit_step "firewall_start" "$port/$proto"
if command -v ufw >/dev/null 2>&1; then
run_cmd ufw allow "$port/$proto" || true
run_cmd ufw --force enable || true
emit_step "firewall_done" "ufw"
return 0
fi
if command -v firewall-cmd >/dev/null 2>&1; then
run_cmd firewall-cmd --permanent --add-port="$port/$proto"
run_cmd firewall-cmd --reload
emit_step "firewall_done" "firewalld"
return 0
fi
warn "$(t firewall_manual_open "$port" "$proto")"
emit_step "firewall_manual" "$port/$proto"
}

50
lib/i18n.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env bash
LOCALE_DIRECTORY="${SCRIPT_DIR}/locales"
load_locale() {
case "${LOCALE,,}" in
ru*|ru_*)
LOCALE="ru"
;;
en*|en_*)
LOCALE="en"
;;
*)
if [[ "${LANGUAGE:-${LANG:-}}" == ru* ]]; then
LOCALE="ru"
else
LOCALE="en"
fi
;;
esac
case "$LOCALE" in
ru)
LOCALE_FILE="$LOCALE_DIRECTORY/ru.sh"
;;
*)
LOCALE_FILE="$LOCALE_DIRECTORY/en.sh"
;;
esac
if [[ ! -f "$LOCALE_FILE" ]]; then
LOCALE="en"
LOCALE_FILE="$LOCALE_DIRECTORY/en.sh"
fi
# shellcheck source=/dev/null
. "$LOCALE_FILE"
}
t() {
local key="$1"
shift || true
local fn="locale_msg_${key}"
if declare -F "$fn" >/dev/null 2>&1; then
"$fn" "$@"
else
echo "$key"
fi
}

77
lib/java.sh Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
is_java_core() {
local core="$1"
case "$core" in
paper|purpur|velocity|powernukkitx) return 0 ;;
*) return 1 ;;
esac
}
install_java_21() {
if command -v java >/dev/null 2>&1; then
local version
version="$(java -version 2>&1 | sed -n 's/.*version "\(.*\)".*/\1/p' | head -n1 | sed 's/"//g' | sed 's/^\([0-9][0-9]*\).*/\1/')"
if [[ "$version" =~ ^[0-9]+$ ]] && (( version >= 21 )); then
info "$(t java_already_available "$version")"
return 0
fi
fi
info "$(t installing_java_21)"
case "$PKG_MANAGER" in
apt)
emit_step "java_install" "apt"
run_cmd apt-get install -y openjdk-21-jre-headless
;;
dnf|yum)
emit_step "java_install" "$PKG_MANAGER"
run_cmd "$PKG_MANAGER" install -y java-21-openjdk-headless
;;
pacman)
emit_step "java_install" "pacman"
run_cmd pacman -Sy --noconfirm jre-openjdk-headless
;;
esac
}
create_java_start_script() {
local dir="$1"
local jar_file="$2"
local ram="$3"
local extra_opts="${4:-}"
local script="$dir/start.sh"
emit_step "render_start_script" "$jar_file"
run_cmd bash -c "cat > '$script' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
exec java -Xms${ram} -Xmx${ram} ${extra_opts} -jar ${jar_file} nogui
EOF"
run_cmd chmod +x "$script"
emit_step "chmod_start_script" "$script"
}
create_powernukkit_start_script() {
local dir="$1"
local ram="$2"
local script="$dir/start.sh"
emit_step "render_start_script" "powernukkitx.jar"
run_cmd bash -c "cat > '$script' <<EOF
#!/usr/bin/env bash
set -e
cd \"\$(dirname \"\$0\")\"
exec java -Xms${ram} -Xmx${ram} \\
-Dfile.encoding=UTF-8 \\
-Djansi.passthrough=true \\
-Dterminal.ansi=true \\
--add-opens java.base/java.lang=ALL-UNNAMED \\
--add-opens java.base/java.io=ALL-UNNAMED \\
--add-opens java.base/java.net=ALL-UNNAMED \\
-jar powernukkitx.jar
EOF"
run_cmd chmod +x "$script"
emit_step "chmod_start_script" "$script"
}

38
lib/logger.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
log() {
local level="$1"
shift
printf '[%s] [%s] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$level" "$*"
}
info() {
log "INFO" "$@"
}
warn() {
log "WARN" "$@"
}
error() {
log "ERROR" "$@"
}
debug() {
if [[ "${DEBUG:-no}" == "yes" ]]; then
log "DEBUG" "$@"
fi
}
fail() {
error "$@"
exit 1
}
run_cmd() {
if [[ "${DRY_RUN:-no}" == "yes" ]]; then
info "[dry-run] $*"
return 0
fi
"$@"
}

34
lib/packages.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
install_base_packages() {
emit_step "packages_start" "$(t installing_base_packages)"
local packages=("curl" "wget" "ca-certificates" "jq" "tar" "unzip" "tmux" "screen" "lsof")
local pkg_string
pkg_string="$(printf '%s ' "${packages[@]}")"
if [[ "$DRY_RUN" == "yes" ]]; then
emit_step "packages_dry_run" "$pkg_string"
info "$(t dry_run_install "$pkg_string")"
return 0
fi
case "$PKG_MANAGER" in
apt)
run_cmd apt-get update
emit_step "packages_index" "apt"
run_cmd DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
;;
dnf|yum)
emit_step "packages_index" "$PKG_MANAGER"
run_cmd "$PKG_MANAGER" install -y "${packages[@]}"
;;
pacman)
emit_step "packages_index" "pacman"
run_cmd pacman -Sy --noconfirm "${packages[@]}"
;;
*)
fail "$(t package_manager_unsupported "$PKG_MANAGER")"
;;
esac
}

72
lib/report.sh Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
json_escape() {
local raw="${1//\\/\\\\}"
raw="${raw//\"/\\\"}"
raw="${raw//$'\n'/\\n}"
echo "$raw"
}
send_report() {
local status="$1"
local message="${2:-}"
local step="${3:-}"
local step_index="${4:-}"
local step_total="${5:-}"
local step_percent="null"
local step_index_json="null"
local step_total_json="null"
local step_percent_json="null"
if [[ "$step_index" =~ ^[0-9]+$ ]]; then
step_index_json="$step_index"
fi
if [[ "$step_total" =~ ^[0-9]+$ && "$step_total" -gt 0 ]]; then
step_total_json="$step_total"
fi
if [[ "$step_index_json" != "null" && "$step_total_json" != "null" ]]; then
if (( step_index > step_total )); then
step_percent_json=100
else
step_percent=$(( step_index * 100 / step_total ))
step_percent_json="$step_percent"
fi
fi
if [[ -z "${REPORT_URL:-}" || -z "${REPORT_TOKEN:-}" ]]; then
return 0
fi
local payload
payload="$(printf '{ "status":"%s", "core":"%s", "dir":"%s", "port":%s, "protocol":"%s", "service":"%s", "message":"%s", "step":"%s", "step_index":%s, "step_total":%s, "step_percent":%s }' \
"$(json_escape "$status")" \
"$(json_escape "${CORE}")" \
"$(json_escape "${SERVER_DIR}")" \
"${SERVER_PORT}" \
"$(json_escape "${CORE_PROTOCOL}")" \
"$(json_escape "${SERVICE_MODE_ACTIVE}")" \
"$(json_escape "$message")" \
"$(json_escape "$step")" \
"$step_index_json" \
"$step_total_json" \
"$step_percent_json")"
local attempt
for attempt in 1 2; do
if [[ "${DRY_RUN}" == "yes" ]]; then
info "$(t report_dry_run "$payload")"
return 0
fi
if curl -fsSL -X POST "$REPORT_URL" \
-H "Authorization: Bearer ${REPORT_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
warn "$(t report_failed)"
}

30
lib/rollback.sh Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
ROLLBACK_IN_PROGRESS="no"
rollback_on_error() {
local exit_code="${1:-1}"
local command_failed="${2:-unknown}"
if [[ "$ROLLBACK_IN_PROGRESS" == "yes" ]]; then
return
fi
ROLLBACK_IN_PROGRESS="yes"
local fail_message
fail_message="$(t deployment_failed "$exit_code" "$command_failed")"
error "$fail_message"
send_report "failed" "$fail_message"
warn "$(t rollback_starts "$SERVER_DIR")"
if [[ "${SERVICE_CREATED:-no}" == "yes" ]]; then
remove_service_if_exists
fi
if [[ -n "${LOG_FILE:-}" ]]; then
info "$(t see_log "$LOG_FILE")"
fi
exit "$exit_code"
}

132
lib/services.sh Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
SERVICE_NAME=""
SERVICE_FILE=""
SERVICE_CREATED="no"
SERVICE_MODE_ACTIVE="none"
stop_service_if_running() {
local service_name="${1:-${SERVICE_NAME:-${CORE}_minecraft}}"
if [[ "$INIT_SYSTEM" != "systemd" ]]; then
return 0
fi
if [[ "${DRY_RUN:-no}" == "yes" ]]; then
info "[dry-run] systemctl stop ${service_name}"
return 0
fi
emit_step "service_stop" "$service_name"
systemctl stop "$service_name" >/dev/null 2>&1 || true
}
create_service() {
local core="$1"
local dir="$2"
local mode="$3"
local start_script="$4"
local start_script_escaped="$4"
start_script_escaped="${start_script_escaped//\\/\\\\}"
start_script_escaped="${start_script_escaped//\"/\\\"}"
start_script_escaped="${start_script_escaped//%/%%}"
SERVICE_NAME="${core}_minecraft"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [[ "$mode" == "none" || "$INIT_SYSTEM" != "systemd" ]]; then
warn "$(t service_create_skipped "$mode" "$INIT_SYSTEM")"
SERVICE_MODE_ACTIVE="none"
return 0
fi
emit_step "service_create" "$core:$mode"
if [[ "$mode" == "tmux-systemd" ]]; then
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server (tmux)
After=network.target
[Service]
Type=forking
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart=/usr/bin/tmux new-session -d -s ${SERVICE_NAME} \"${start_script_escaped}\"
ExecStop=/usr/bin/tmux send-keys -t ${SERVICE_NAME} 'stop' C-m
ExecStopPost=/bin/sleep 8
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
elif [[ "$mode" == "screen" ]]; then
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server (screen)
After=network.target
[Service]
Type=forking
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart=/usr/bin/screen -dmS ${SERVICE_NAME} \"${start_script_escaped}\"
ExecStop=/usr/bin/screen -S ${SERVICE_NAME} -X quit
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
else
run_cmd bash -c "cat > '$SERVICE_FILE' <<EOF
[Unit]
Description=Minecraft ${core} server
After=network.target
[Service]
Type=simple
User=${SERVER_USER}
Group=${SERVER_USER}
WorkingDirectory=${dir}
ExecStart="${start_script_escaped}"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF"
fi
SERVICE_CREATED="yes"
SERVICE_MODE_ACTIVE="$mode"
emit_step "service_reload_enable" "$SERVICE_NAME"
run_cmd systemctl daemon-reload
run_cmd systemctl enable "$SERVICE_NAME"
}
start_service() {
if [[ "$SERVICE_MODE_ACTIVE" == "none" ]]; then
info "$(t service_start_skipped "$SERVICE_MODE_ACTIVE")"
return 0
fi
emit_step "service_start" "$SERVICE_NAME"
run_cmd systemctl restart "$SERVICE_NAME"
}
remove_service_if_exists() {
if [[ "$SERVICE_CREATED" != "yes" ]]; then
return 0
fi
if [[ -n "$SERVICE_NAME" ]]; then
run_cmd systemctl stop "$SERVICE_NAME" || true
run_cmd systemctl disable "$SERVICE_NAME" || true
fi
if [[ -n "${SERVICE_FILE}" && -f "$SERVICE_FILE" ]]; then
run_cmd rm -f "$SERVICE_FILE"
fi
run_cmd systemctl daemon-reload || true
}

31
lib/users.sh Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
create_minecraft_user() {
local user="$1"
if [[ "$user" == "root" ]]; then
fail "$(t refuse_root_user)"
fi
if ! id "$user" >/dev/null 2>&1; then
emit_step "user_create" "$user"
info "$(t creating_user "$user")"
if [[ "$DRY_RUN" == "no" ]]; then
useradd -r -m -s /bin/bash "$user"
else
info "$(t dry_run_useradd "$user")"
fi
else
emit_step "user_exists" "$user"
fi
}
prepare_server_dir() {
local dir="$1"
local user="$2"
emit_step "prepare_dir" "$dir"
run_cmd mkdir -p "$dir"
run_cmd chown -R "${user}:${user}" "$dir"
emit_step "permission_fix" "$dir"
}

View File

@@ -7,12 +7,12 @@ validate_root_usage() {
} }
validate_core_support() { validate_core_support() {
local supported_cores="pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc" local supported_cores="pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc,bds"
local supported_os="ubuntu,debian" local supported_os="ubuntu,debian"
local planned_os="arch,fedora,rocky,almalinux" local planned_os="arch,fedora,rocky,almalinux"
case "$CORE" in case "$CORE" in
pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc) pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds)
;; ;;
*) *)
fail "$(t unsupported_core "$CORE" "$supported_cores" "${SCRIPT_DIR}/manifests/cores.json")" fail "$(t unsupported_core "$CORE" "$supported_cores" "${SCRIPT_DIR}/manifests/cores.json")"

View File

@@ -85,12 +85,13 @@ locale_msg_port_out_of_range() { echo "Port out of range: $1"; }
locale_msg_invalid_service_mode() { echo "Invalid service mode: $1"; } locale_msg_invalid_service_mode() { echo "Invalid service mode: $1"; }
locale_msg_invalid_firewall_value() { echo "Invalid --firewall value: $1"; } locale_msg_invalid_firewall_value() { echo "Invalid --firewall value: $1"; }
locale_msg_prompt_core() { echo "Core (pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc)"; } locale_msg_prompt_core() { echo "Core (pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds)"; }
locale_msg_prompt_server_directory() { echo "Server directory"; } locale_msg_prompt_server_directory() { echo "Server directory"; }
locale_msg_prompt_server_port() { echo "Server port"; } locale_msg_prompt_server_port() { echo "Server port"; }
locale_msg_prompt_ram() { echo "RAM"; } locale_msg_prompt_ram() { echo "RAM"; }
locale_msg_prompt_php_build() { echo "PHP build tag (latest|tag)"; } locale_msg_prompt_php_build() { echo "PHP build tag (latest|tag)"; }
locale_msg_prompt_pocketmine_version() { echo "PocketMine-MCPE version/tag (latest|tag)"; } locale_msg_prompt_pocketmine_version() { echo "PocketMine-MCPE version/tag (latest|tag)"; }
locale_msg_prompt_bds_version() { echo "Bedrock Dedicated Server version (latest|version)"; }
locale_msg_prompt_service_mode() { echo "Service mode"; } locale_msg_prompt_service_mode() { echo "Service mode"; }
locale_msg_prompt_open_firewall() { echo "Open firewall (yes/no)"; } locale_msg_prompt_open_firewall() { echo "Open firewall (yes/no)"; }
locale_msg_prompt_install_user() { echo "Install user"; } locale_msg_prompt_install_user() { echo "Install user"; }
@@ -105,6 +106,7 @@ locale_msg_easy_plan_user() { echo "Service user: $1"; }
locale_msg_easy_plan_firewall() { echo "Firewall: $1"; } locale_msg_easy_plan_firewall() { echo "Firewall: $1"; }
locale_msg_easy_plan_build() { echo "PHP build: $1"; } locale_msg_easy_plan_build() { echo "PHP build: $1"; }
locale_msg_easy_plan_mc_version() { echo "PocketMine-MCPE tag: $1"; } locale_msg_easy_plan_mc_version() { echo "PocketMine-MCPE tag: $1"; }
locale_msg_easy_plan_bds_version() { echo "BDS version: $1"; }
locale_msg_easy_plan_eula() { echo "EULA: $1"; } locale_msg_easy_plan_eula() { echo "EULA: $1"; }
locale_msg_dir_not_empty_auto() { echo "Directory '$1' is not empty. Add --allow-existing, --backup-existing or --force-clean."; } locale_msg_dir_not_empty_auto() { echo "Directory '$1' is not empty. Add --allow-existing, --backup-existing or --force-clean."; }

View File

@@ -85,12 +85,13 @@ locale_msg_port_out_of_range() { echo "Порт вне диапазона: $1";
locale_msg_invalid_service_mode() { echo "Неподдерживаемый режим сервиса: $1"; } locale_msg_invalid_service_mode() { echo "Неподдерживаемый режим сервиса: $1"; }
locale_msg_invalid_firewall_value() { echo "Недопустимое значение --firewall: $1"; } locale_msg_invalid_firewall_value() { echo "Недопустимое значение --firewall: $1"; }
locale_msg_prompt_core() { echo "Ядро (pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc)"; } locale_msg_prompt_core() { echo "Ядро (pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc|bds)"; }
locale_msg_prompt_server_directory() { echo "Каталог сервера"; } locale_msg_prompt_server_directory() { echo "Каталог сервера"; }
locale_msg_prompt_server_port() { echo "Порт сервера"; } locale_msg_prompt_server_port() { echo "Порт сервера"; }
locale_msg_prompt_ram() { echo "RAM"; } locale_msg_prompt_ram() { echo "RAM"; }
locale_msg_prompt_php_build() { echo "Тег PHP-билда (latest|tag)"; } locale_msg_prompt_php_build() { echo "Тег PHP-билда (latest|tag)"; }
locale_msg_prompt_pocketmine_version() { echo "Версия/тег PocketMine-MCPE (latest|tag)"; } locale_msg_prompt_pocketmine_version() { echo "Версия/тег PocketMine-MCPE (latest|tag)"; }
locale_msg_prompt_bds_version() { echo "Версия Bedrock Dedicated Server (latest|версия)"; }
locale_msg_prompt_service_mode() { echo "Режим сервиса"; } locale_msg_prompt_service_mode() { echo "Режим сервиса"; }
locale_msg_prompt_open_firewall() { echo "Открыть firewall (yes/no)"; } locale_msg_prompt_open_firewall() { echo "Открыть firewall (yes/no)"; }
locale_msg_prompt_install_user() { echo "Пользователь для установки"; } locale_msg_prompt_install_user() { echo "Пользователь для установки"; }
@@ -105,6 +106,7 @@ locale_msg_easy_plan_user() { echo "Пользователь сервиса: $1"
locale_msg_easy_plan_firewall() { echo "Firewall: $1"; } locale_msg_easy_plan_firewall() { echo "Firewall: $1"; }
locale_msg_easy_plan_build() { echo "PHP-билд: $1"; } locale_msg_easy_plan_build() { echo "PHP-билд: $1"; }
locale_msg_easy_plan_mc_version() { echo "Версия/тег PocketMine-MCPE: $1"; } locale_msg_easy_plan_mc_version() { echo "Версия/тег PocketMine-MCPE: $1"; }
locale_msg_easy_plan_bds_version() { echo "Версия BDS: $1"; }
locale_msg_easy_plan_eula() { echo "EULA: $1"; } locale_msg_easy_plan_eula() { echo "EULA: $1"; }
locale_msg_dir_not_empty_auto() { echo "Каталог '$1' не пустой. Укажите --allow-existing, --backup-existing или --force-clean."; } locale_msg_dir_not_empty_auto() { echo "Каталог '$1' не пустой. Укажите --allow-existing, --backup-existing или --force-clean."; }

View File

@@ -62,5 +62,15 @@
"default_dir": "/opt/minecraft/nethergamesmc", "default_dir": "/opt/minecraft/nethergamesmc",
"supports_protocols": ["udp"], "supports_protocols": ["udp"],
"supported_services": ["systemd", "tmux-systemd", "screen", "none"] "supported_services": ["systemd", "tmux-systemd", "screen", "none"]
},
"bds": {
"name": "Minecraft Bedrock Dedicated Server",
"edition": "bedrock",
"runtime": "native",
"default_port": 19132,
"protocol": "udp",
"default_dir": "/opt/minecraft/bds",
"supports_protocols": ["udp"],
"supported_services": ["systemd", "tmux-systemd", "screen", "none"]
} }
} }

15
manifests/os-support.json Normal file
View File

@@ -0,0 +1,15 @@
{
"supports": {
"mvp": ["ubuntu", "debian"],
"planned": ["arch", "fedora", "rocky", "almalinux"]
},
"service": {
"systemd": ["ubuntu", "debian", "fedora", "rocky", "almalinux"]
},
"java_21": {
"apt": "openjdk-21-jre-headless",
"dnf": "java-21-openjdk-headless",
"yum": "java-21-openjdk-headless",
"pacman": "jre-openjdk-headless"
}
}

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Minecraft {{CORE_NAME}} server
After=network.target
[Service]
Type=simple
User={{SERVER_USER}}
Group={{SERVER_USER}}
WorkingDirectory={{SERVER_DIR}}
ExecStart={{START_SCRIPT}}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Minecraft {{CORE_NAME}} server (tmux)
After=network.target
[Service]
Type=forking
User={{SERVER_USER}}
Group={{SERVER_USER}}
WorkingDirectory={{SERVER_DIR}}
ExecStart=/usr/bin/tmux new-session -d -s {{SERVICE_NAME}} '{{START_SCRIPT}}'
ExecStop=/usr/bin/tmux send-keys -t {{SERVICE_NAME}} 'stop' C-m
ExecStopPost=/bin/sleep 8
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

4
templates/start-java.tpl Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
exec java -Xms{{RAM}} -Xmx{{RAM}} {{JAVA_OPTS}} -jar {{JAR_FILE}}

4
templates/start-pmmp.tpl Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
./start.sh "$@"