From 50b5f99cbdb63693ac437d8d103bb5e718bbbc17 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:37:02 +0900 Subject: [PATCH 1/4] feat: import server properties with per-server ownership --- libexec/mcserver-kit/home-tui.sh | 24 +++- libexec/mcserver-kit/server-config.py | 99 +++++++++++++++- libexec/mcserver-kit/server-manager.sh | 63 +++++++++++ libexec/mcserver-kit/server-properties-tui.sh | 31 ++++- libexec/mcserver-kit/windows-dialog.ps1 | 14 ++- share/mcserver-kit/locales/en.json | 17 ++- share/mcserver-kit/locales/ja.json | 17 ++- tests/run-tests.sh | 106 ++++++++++++++++++ 8 files changed, 362 insertions(+), 9 deletions(-) diff --git a/libexec/mcserver-kit/home-tui.sh b/libexec/mcserver-kit/home-tui.sh index d7f397e..a1cfe6b 100755 --- a/libexec/mcserver-kit/home-tui.sh +++ b/libexec/mcserver-kit/home-tui.sh @@ -131,15 +131,18 @@ run_and_show() { } server_action_menu() { - local id="$1" directory="$2" choice + local id="$1" directory="$2" choice output while true; do - choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 21 78 11 \ + choice="$(whiptail --title "$id" --menu "$(tr home.server_status "$(server_status "$directory")")" 23 78 13 \ start "$(tr home.start)" \ stop "$(tr home.stop)" \ restart "$(tr home.restart)" \ status "$(tr home.status)" \ logs "$(tr home.logs)" \ properties "$(tr home.properties)" \ + import-properties "$(tr home.import_properties)" \ + open-data "$(tr home.open_data)" \ + open-server "$(tr home.open_server)" \ down "$(tr home.down)" \ back "$(tr tui.back)" \ 3>&1 1>&2 2>&3)" || return @@ -155,6 +158,23 @@ server_action_menu() { properties) "${SCRIPT_DIR}/server-manager.sh" server "$id" properties || true ;; + import-properties) + if whiptail --yesno "$(tr home.import_confirm "$id")" 12 76; then + clear + "${SCRIPT_DIR}/server-manager.sh" server "$id" import-properties || true + pause_for_enter + fi + ;; + open-data | open-server) + new_temp_file output + if [[ "$choice" == open-data ]]; then + "${SCRIPT_DIR}/server-manager.sh" server "$id" open data >"$output" 2>&1 || + whiptail --title "$(tr common.error)" --textbox "$output" 14 78 + else + "${SCRIPT_DIR}/server-manager.sh" server "$id" open server >"$output" 2>&1 || + whiptail --title "$(tr common.error)" --textbox "$output" 14 78 + fi + ;; down) if whiptail --yesno "$(tr home.down_confirm "$id")" 10 72; then run_and_show "$id" "${SCRIPT_DIR}/server-manager.sh" server "$id" down diff --git a/libexec/mcserver-kit/server-config.py b/libexec/mcserver-kit/server-config.py index fa6c696..c41ab14 100755 --- a/libexec/mcserver-kit/server-config.py +++ b/libexec/mcserver-kit/server-config.py @@ -9,6 +9,7 @@ import shutil import sys import tempfile +from datetime import datetime from pathlib import Path MANAGED_DEFAULTS = { @@ -116,6 +117,68 @@ def read_properties(path: Path) -> dict[str, str]: return values +def set_property(path: Path, env_key: str, value: str) -> None: + property_key = PROPERTY_KEYS[env_key] + if "\n" in value or "\r" in value: + raise ValueError("Property values cannot contain newlines") + lines = path.read_text(encoding="utf-8").splitlines(keepends=True) + replacement = f"{property_key}={value}\n" + indices = [ + index for index, line in enumerate(lines) + if line.partition("=")[0].strip() == property_key + ] + if indices: + lines[indices[-1]] = replacement + else: + if lines and not lines[-1].endswith("\n"): + lines[-1] += "\n" + lines.append(replacement) + atomic_write(path, "".join(lines)) + + +def validate_properties_source(source: Path) -> None: + if not source.is_file(): + raise ValueError(f"Not a file: {source}") + if source.name != "server.properties": + raise ValueError("Select a file named server.properties") + if not read_properties(source): + raise ValueError("The selected file has no property entries") + + +def import_properties(server_dir: Path, source: Path) -> Path | None: + validate_properties_source(source) + + destination = server_dir / "data" / "server.properties" + destination.parent.mkdir(parents=True, exist_ok=True) + if source.resolve() == destination.resolve(): + raise ValueError("The source is already this server's server.properties") + backup = None + if destination.exists(): + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.bak") + suffix = 1 + while backup.exists(): + backup = destination.with_name(f"server.properties.mcserver-kit.{stamp}.{suffix}.bak") + suffix += 1 + shutil.copy2(destination, backup) + + descriptor, temporary_name = tempfile.mkstemp(prefix="server.properties.", dir=destination.parent) + try: + with os.fdopen(descriptor, "wb") as output, source.open("rb") as input_file: + shutil.copyfileobj(input_file, output) + os.chmod(temporary_name, 0o644) + env_path = server_dir / "server.env" + values = read_env(env_path) + values["OVERRIDE_SERVER_PROPERTIES"] = "false" + write_env(env_path, values) + os.replace(temporary_name, destination) + finally: + if os.path.exists(temporary_name): + os.unlink(temporary_name) + + return backup + + def compose_environment(path: Path, dotenv: dict[str, str]) -> dict[str, str]: values: dict[str, str] = {} if not path.is_file(): @@ -195,7 +258,7 @@ def migrate(server_dir: Path) -> None: backup = server_dir / "compose.yaml.mcserver-kit.bak" if not target.exists() and not backup.exists(): shutil.copy2(compose, backup) - values: dict[str, str] = {} + values: dict[str, str] = dict(existing) for env_key, default in MANAGED_DEFAULTS.items(): property_key = PROPERTY_KEYS.get(env_key, "") values[env_key] = existing.get( @@ -214,13 +277,45 @@ def migrate(server_dir: Path) -> None: def main() -> int: if len(sys.argv) < 3: - print("usage: server-config.py get|set|migrate TARGET [KEY] [VALUE]", file=sys.stderr) + print("usage: server-config.py get|set|migrate|property-get|property-set|import-properties TARGET [KEY] [VALUE]", file=sys.stderr) return 2 operation = sys.argv[1] target = Path(sys.argv[2]) if operation == "migrate" and len(sys.argv) == 3: migrate(target) return 0 + if operation == "validate-properties" and len(sys.argv) == 3: + try: + validate_properties_source(target) + except (OSError, ValueError, UnicodeError) as error: + print(f"Invalid server.properties: {error}", file=sys.stderr) + return 1 + return 0 + if operation == "import-properties" and len(sys.argv) == 4: + try: + backup = import_properties(target, Path(sys.argv[3])) + except (OSError, ValueError, UnicodeError) as error: + print(f"Import failed: {error}", file=sys.stderr) + return 1 + if backup: + print(backup) + return 0 + if operation == "property-get" and len(sys.argv) in (4, 5): + key = PROPERTY_KEYS.get(sys.argv[3]) + if key is None: + return 2 + default = sys.argv[4] if len(sys.argv) == 5 else "" + print(read_properties(target).get(key, default)) + return 0 + if operation == "property-set" and len(sys.argv) == 5: + if sys.argv[3] not in PROPERTY_KEYS: + return 2 + try: + set_property(target, sys.argv[3], sys.argv[4]) + except (OSError, ValueError, UnicodeError) as error: + print(f"Could not edit server.properties: {error}", file=sys.stderr) + return 1 + return 0 if operation == "get" and len(sys.argv) in (4, 5): values = read_env(target) default = sys.argv[4] if len(sys.argv) == 5 else "" diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index a52321f..cb61a85 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -6,6 +6,8 @@ set -Eeuo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${MCSERVER_KIT_CONFIG:-${HOME}/.config/mcserver-compose-kit/config.yml}" CONFIG_VALUE="${SCRIPT_DIR}/config-value.py" +CONFIG_TOOL="${SCRIPT_DIR}/server-config.py" +WINDOWS_DIALOG="${SCRIPT_DIR}/windows-dialog.ps1" # shellcheck source=libexec/mcserver-kit/i18n.sh source "${SCRIPT_DIR}/i18n.sh" @@ -82,6 +84,59 @@ compose_in() { ) } +require_stopped() { + local directory="$1" running + running="$(compose_in "$directory" ps --status running --services)" || die "$(tr server.status_failed)" + [[ -z "$running" ]] || die "$(tr server.stop_before_import)" +} + +choose_properties_file() { + local enabled dialog_path encoded selected + enabled="$(python3 "$CONFIG_VALUE" get "$CONFIG_FILE" ui windows_dialogs 2>/dev/null || printf true)" + if [[ "$enabled" == true ]] && command -v wslpath >/dev/null 2>&1 && + [[ -x /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe && -f "$WINDOWS_DIALOG" ]]; then + dialog_path="$(wslpath -w "$WINDOWS_DIALOG")" + encoded="$(/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe \ + -NoProfile -ExecutionPolicy Bypass -File "$dialog_path" -Mode SelectProperties 2>/dev/null)" || return 1 + encoded="${encoded//$'\r'/}" + selected="$(printf '%s' "$encoded" | base64 --decode)" || return 1 + wslpath -u "$selected" + return + fi + [[ -t 0 ]] || die "$(tr server.import_usage)" + read -r -p "$(tr server.import_path_prompt)" selected + [[ -n "$selected" ]] || return 1 + printf '%s' "$selected" +} + +import_properties() { + local directory="$1" source="${2-}" backup + require_stopped "$directory" + if [[ -z "$source" ]]; then + source="$(choose_properties_file)" || return 0 + fi + [[ -f "$source" ]] || die "$(tr server.import_source_missing "$source")" + python3 "$CONFIG_TOOL" validate-properties "$source" || die "$(tr server.import_failed)" + python3 "$CONFIG_TOOL" migrate "$directory" || die "$(tr server.import_failed)" + backup="$(python3 "$CONFIG_TOOL" import-properties "$directory" "$source")" || die "$(tr server.import_failed)" + compose_in "$directory" config --quiet || die "$(tr properties.compose_invalid)" + [[ -z "$backup" ]] || printf '%s\n' "$(tr server.import_backup "$backup")" + printf '%s\n' "$(tr server.import_done "${directory}/data/server.properties")" +} + +open_folder() { + local directory="$1" part="${2:-data}" target windows_path + case "$part" in + data) target="${directory}/data" ;; + server) target="$directory" ;; + *) die "$(tr server.open_usage)" ;; + esac + [[ -d "$target" ]] || die "$(tr server.open_missing "$target")" + command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1 || die "$(tr server.explorer_unavailable)" + windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)" + explorer.exe "$windows_path" +} + manage_server() { local id="${1-}" local action="${2-}" @@ -124,6 +179,14 @@ manage_server() { properties) exec "${SCRIPT_DIR}/server-properties-tui.sh" "$id" "$directory" ;; + import-properties) + [[ $# -le 1 ]] || die "$(tr server.import_usage)" + import_properties "$directory" "${1-}" + ;; + open) + [[ $# -le 1 ]] || die "$(tr server.open_usage)" + open_folder "$directory" "${1:-data}" + ;; *) die "$(tr server.unknown_action "$action")" ;; diff --git a/libexec/mcserver-kit/server-properties-tui.sh b/libexec/mcserver-kit/server-properties-tui.sh index 46dd053..9d67445 100755 --- a/libexec/mcserver-kit/server-properties-tui.sh +++ b/libexec/mcserver-kit/server-properties-tui.sh @@ -7,8 +7,10 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SERVER_ID="${1-}" SERVER_DIR="${2-}" SERVER_ENV="${SERVER_DIR}/server.env" +SERVER_PROPERTIES="${SERVER_DIR}/data/server.properties" CONFIG_TOOL="${SCRIPT_DIR}/server-config.py" changed=false +manual_properties=false # shellcheck source=libexec/mcserver-kit/i18n.sh source "${SCRIPT_DIR}/i18n.sh" @@ -40,11 +42,27 @@ die() { } setting_get() { - python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" + if [[ "$manual_properties" == true ]]; then + case "$1" in + WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE) + python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" ;; + *) python3 "$CONFIG_TOOL" property-get "$SERVER_PROPERTIES" "$1" "$2" ;; + esac + else + python3 "$CONFIG_TOOL" get "$SERVER_ENV" "$1" "$2" + fi } setting_set() { - python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" + if [[ "$manual_properties" == true ]]; then + case "$1" in + WHITELIST | EXISTING_WHITELIST_FILE | OPS | EXISTING_OPS_FILE) + python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" ;; + *) python3 "$CONFIG_TOOL" property-set "$SERVER_PROPERTIES" "$1" "$2" ;; + esac + else + python3 "$CONFIG_TOOL" set "$SERVER_ENV" "$1" "$2" + fi changed=true } @@ -212,6 +230,15 @@ main() { whiptail --yesno "$(tr properties.migration_prompt)" 11 76 || return fi python3 "$CONFIG_TOOL" migrate "$SERVER_DIR" + if [[ "$(python3 "$CONFIG_TOOL" get "$SERVER_ENV" OVERRIDE_SERVER_PROPERTIES true)" == false ]]; then + [[ -f "$SERVER_PROPERTIES" ]] || die "$(tr properties.file_missing)" + local running + running="$(cd "$SERVER_DIR" && docker compose ps --status running --services)" || die "$(tr server.status_failed)" + if [[ -n "$running" ]]; then + die "$(tr properties.stop_first "$SERVER_ID")" + fi + manual_properties=true + fi (cd "$SERVER_DIR" && docker compose config --quiet) || die "$(tr properties.compose_invalid)" while true; do diff --git a/libexec/mcserver-kit/windows-dialog.ps1 b/libexec/mcserver-kit/windows-dialog.ps1 index 5899e1f..d4db1ff 100644 --- a/libexec/mcserver-kit/windows-dialog.ps1 +++ b/libexec/mcserver-kit/windows-dialog.ps1 @@ -1,6 +1,6 @@ param( [Parameter(Mandatory = $true)] - [ValidateSet('SelectWorld', 'InputMotd', 'Check')] + [ValidateSet('SelectWorld', 'SelectProperties', 'InputMotd', 'Check')] [string]$Mode, [string]$DefaultValue = '' @@ -58,6 +58,18 @@ if ($Mode -eq 'SelectWorld') { exit 0 } +if ($Mode -eq 'SelectProperties') { + $dialog = New-Object System.Windows.Forms.OpenFileDialog + $dialog.Title = 'Select server.properties / server.propertiesを選択' + $dialog.Filter = 'server.properties|server.properties|All files (*.*)|*.*' + $dialog.CheckFileExists = $true + if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { + exit 1 + } + Write-Utf8Base64 $dialog.FileName + exit 0 +} + if ($Mode -eq 'InputMotd') { $form = New-Object System.Windows.Forms.Form $form.Text = 'Minecraft Server Kit' diff --git a/share/mcserver-kit/locales/en.json b/share/mcserver-kit/locales/en.json index cc69484..ccc1a89 100644 --- a/share/mcserver-kit/locales/en.json +++ b/share/mcserver-kit/locales/en.json @@ -40,6 +40,10 @@ "home.status": "Show status", "home.logs": "Follow logs", "home.properties": "Server settings", + "home.import_properties": "Import server.properties", + "home.import_confirm": "Import server.properties for %s? Stop the server first. The current file will be backed up, and this server will use data/server.properties for property settings.", + "home.open_data": "Open persistent data in Explorer", + "home.open_server": "Open server folder in Explorer", "home.down": "Remove containers", "home.down_confirm": "Remove containers and networks for %s? World data is preserved.", "home.diagnostics_title": "Environment diagnostics", @@ -130,7 +134,18 @@ "server.running": "running", "server.stopped": "stopped", "server.none": "(no servers)", - "server.usage": "Usage: mcserver-kit server SERVER_ID start|stop|restart|status|logs|down|properties", + "server.usage": "Usage: mcserver-kit server SERVER_ID start|stop|restart|status|logs|down|properties|import-properties|open", + "server.status_failed": "Could not check whether the server is running. Check Docker and try again.", + "server.stop_before_import": "Stop the server before importing server.properties.", + "server.import_usage": "Usage: mcserver-kit server SERVER_ID import-properties [PATH]", + "server.import_path_prompt": "Path to server.properties: ", + "server.import_source_missing": "File not found: %s", + "server.import_failed": "Could not import server.properties.", + "server.import_backup": "Previous server.properties backup: %s", + "server.import_done": "Imported to %s. This server now uses data/server.properties as the source of truth for property settings.", + "server.open_usage": "Usage: mcserver-kit server SERVER_ID open [data|server]", + "server.open_missing": "Folder not found: %s", + "server.explorer_unavailable": "Windows Explorer is unavailable. This action requires WSL interop and wslpath.", "server.validating": "Validating Docker Compose configuration for %s...", "server.starting": "Starting %s...", "server.stopping": "Stopping %s...", diff --git a/share/mcserver-kit/locales/ja.json b/share/mcserver-kit/locales/ja.json index 3280b12..a322c55 100644 --- a/share/mcserver-kit/locales/ja.json +++ b/share/mcserver-kit/locales/ja.json @@ -40,6 +40,10 @@ "home.status": "状態を表示", "home.logs": "ログを表示", "home.properties": "サーバー設定", + "home.import_properties": "server.propertiesをインポート", + "home.import_confirm": "%sにserver.propertiesをインポートしますか?先にサーバーを停止してください。既存ファイルをバックアップし、このサーバーのプロパティ設定はdata/server.propertiesを正本に切り替えます。", + "home.open_data": "永続データをExplorerで開く", + "home.open_server": "サーバーフォルダをExplorerで開く", "home.down": "コンテナを削除", "home.down_confirm": "%sのコンテナとネットワークを削除しますか?ワールドデータは保持されます。", "home.diagnostics_title": "動作環境の診断", @@ -130,7 +134,18 @@ "server.running": "起動中", "server.stopped": "停止中", "server.none": "(サーバーがありません)", - "server.usage": "使い方: mcserver-kit server サーバーID start|stop|restart|status|logs|down|properties", + "server.usage": "使い方: mcserver-kit server サーバーID start|stop|restart|status|logs|down|properties|import-properties|open", + "server.status_failed": "サーバーが起動中か確認できませんでした。Dockerを確認して再試行してください。", + "server.stop_before_import": "server.propertiesのインポート前にサーバーを停止してください。", + "server.import_usage": "使い方: mcserver-kit server サーバーID import-properties [パス]", + "server.import_path_prompt": "server.propertiesのパス: ", + "server.import_source_missing": "ファイルが見つかりません: %s", + "server.import_failed": "server.propertiesをインポートできませんでした。", + "server.import_backup": "元のserver.propertiesのバックアップ: %s", + "server.import_done": "%sにインポートしました。このサーバーのプロパティ設定はdata/server.propertiesが正本になります。", + "server.open_usage": "使い方: mcserver-kit server サーバーID open [data|server]", + "server.open_missing": "フォルダが見つかりません: %s", + "server.explorer_unavailable": "Windows Explorerを利用できません。WSL連携とwslpathが必要です。", "server.validating": "%sのDocker Compose設定を検証しています...", "server.starting": "%sを起動しています...", "server.stopping": "%sを停止しています...", diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 8d97bee..a132e0d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -674,6 +674,111 @@ WHIPTAIL assert_equal 'Unified MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "$server_env" MOTD)" 'the properties TUI updates unified settings' } +test_property_import_and_explorer() { + local temp_dir="$1" root + root="${temp_dir}/property-import/servers" + local config_file="${temp_dir}/property-import/config.yml" + local fake_bin="${temp_dir}/property-import/bin" + local source_dir="${temp_dir}/property-import/distribution" + local server_dir="${root}/alpha" output + mkdir -p "${server_dir}/data" "$fake_bin" "$source_dir" + cat >"$config_file" <"${server_dir}/compose.yaml" <<'COMPOSE' +services: + minecraft: + image: itzg/minecraft-server:java21 + env_file: + - server.env + volumes: + - ./data:/data +COMPOSE + cat >"${server_dir}/server.env" <<'ENV' +MOTD="Old MOTD" +DIFFICULTY="easy" +ENV + cat >"${server_dir}/data/server.properties" <<'PROPERTIES' +# original +motd=Old MOTD +PROPERTIES + cat >"${source_dir}/server.properties" <<'PROPERTIES' +# distributed settings +motd=Distributed MOTD +difficulty=hard +custom-setting=preserve-me +PROPERTIES + cat >"${fake_bin}/docker" <<'DOCKER' +#!/usr/bin/env bash +if [[ "$*" == 'compose ps --status running --services' && "${MCSERVER_KIT_TEST_RUNNING:-false}" == true ]]; then + printf 'minecraft\n' +fi +DOCKER + cat >"${fake_bin}/wslpath" <<'WSLPATH' +#!/usr/bin/env bash +printf 'WIN:%s\n' "$2" +WSLPATH + cat >"${fake_bin}/explorer.exe" <<'EXPLORER' +#!/usr/bin/env bash +printf '%s\n' "$1" >"$MCSERVER_KIT_TEST_EXPLORER_LOG" +EXPLORER + chmod +x "${fake_bin}/docker" "${fake_bin}/wslpath" "${fake_bin}/explorer.exe" + + : >"${source_dir}/invalid.properties" + assert_fails 'invalid property input is rejected before migration' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/invalid.properties" + assert_equal 'absent' "$(! grep -q '^MAX_PLAYERS=' "${server_dir}/server.env" && printf absent)" 'invalid import does not migrate server settings' + + assert_fails 'import refuses to replace properties while the server is running' \ + env PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_RUNNING=true bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties" + assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'running server keeps the original properties' + + output="$(PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha import-properties "${source_dir}/server.properties")" + assert_equal 'Distributed MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'import copies the supplied server.properties' + assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'import disables property overrides for this server' + assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'import keeps custom settings' + assert_equal 'present' "$(grep -q 'motd=Old MOTD' "${server_dir}"/data/server.properties.mcserver-kit.*.bak && printf present)" 'import backs up the previous properties' + assert_equal 'present' "$(grep -q 'Previous server.properties backup' <<<"$output" && printf present)" 'import reports the backup location' + + python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" migrate "$server_dir" + assert_equal 'false' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" OVERRIDE_SERVER_PROPERTIES)" 'later migration preserves manual property mode' + python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-set "${server_dir}/data/server.properties" MOTD 'Edited MOTD' + assert_equal 'Edited MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'manual property editor writes the data file' + assert_equal 'present' "$(grep -q 'custom-setting=preserve-me' "${server_dir}/data/server.properties" && printf present)" 'manual property editor preserves unrelated entries' + + cat >"${fake_bin}/whiptail" <<'WHIPTAIL' +#!/usr/bin/env bash +case " $* " in + *' --inputbox '*) printf 'TUI MOTD' >&2 ;; + *' --yesno '*) exit 1 ;; + *' --menu '*) + if [[ -f "$MCSERVER_KIT_TEST_WHIPTAIL_STATE" ]]; then + printf '__exit' >&2 + else + : >"$MCSERVER_KIT_TEST_WHIPTAIL_STATE" + printf 'MOTD' >&2 + fi + ;; + *) exit 0 ;; +esac +WHIPTAIL + chmod +x "${fake_bin}/whiptail" + PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en \ + MCSERVER_KIT_TEST_WHIPTAIL_STATE="${temp_dir}/property-import/whiptail-state" \ + bash "${REPO_ROOT}/libexec/mcserver-kit/server-properties-tui.sh" alpha "$server_dir" >/dev/null + assert_equal 'TUI MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" property-get "${server_dir}/data/server.properties" MOTD)" 'the TUI edits imported properties directly' + assert_equal 'Old MOTD' "$(python3 "${REPO_ROOT}/libexec/mcserver-kit/server-config.py" get "${server_dir}/server.env" MOTD)" 'manual property edits do not rewrite the old environment value' + + PATH="${fake_bin}:$PATH" MCSERVER_KIT_LANG=en MCSERVER_KIT_CONFIG="$config_file" \ + MCSERVER_KIT_TEST_EXPLORER_LOG="${temp_dir}/property-import/explorer.log" \ + bash "${REPO_ROOT}/mcserver-kit" server alpha open data + assert_equal "WIN:${server_dir}/data" "$(<"${temp_dir}/property-import/explorer.log")" 'open data passes the persistent folder to Explorer' +} + main() { TEST_TEMP_DIR="$(mktemp -d)" trap cleanup EXIT @@ -695,6 +800,7 @@ main() { test_config_value_editor "$TEST_TEMP_DIR" test_server_management "$TEST_TEMP_DIR" test_server_property_editor "$TEST_TEMP_DIR" + test_property_import_and_explorer "$TEST_TEMP_DIR" test_home_dashboard "$TEST_TEMP_DIR" printf 'PASS: %d specification tests, %d skipped\n' "$tests_run" "$tests_skipped" From 760473d440e3c8bc3d39a4e2205352d4fcd3aae9 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:37:12 +0900 Subject: [PATCH 2/4] docs: explain properties import and Explorer access --- README-JA.md | 9 ++++++++- README.md | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README-JA.md b/README-JA.md index c9374f7..3d66dc9 100644 --- a/README-JA.md +++ b/README-JA.md @@ -170,6 +170,9 @@ mcserver-kit server logs mcserver-kit server logs --no-follow mcserver-kit server down mcserver-kit server properties +mcserver-kit server import-properties /path/to/server.properties +mcserver-kit server open data +mcserver-kit server open server ``` `stop`と`shutdown`はコンテナを削除せず停止します。`down`はコンテナとネットワークを削除します。いずれもサーバーの`data/`は削除しません。 @@ -184,7 +187,11 @@ mcserver-kit server properties MOTD、難易度、ゲームモード、最大人数、オンラインモード、ホワイトリスト、OP、飛行、コマンドブロック、PvP、描画・シミュレーション距離、スポーン保護、ネザー、Mob/NPC生成、リソースパックなどを編集できます。 -ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。 +通常、ツールが管理する設定の正本は各サーバーの`server.env`です。Docker Composeが値を`itzg/minecraft-server`へ渡し、コンテナ起動時に`server.properties`へ反映します。 + +配布された`server.properties`を使う場合は、サーバーを停止してホーム画面の「server.propertiesをインポート」を選ぶか、`mcserver-kit server import-properties /path/to/server.properties`を実行します。Windowsダイアログを有効にしているWSL環境では、ホーム画面からWindowsのファイル選択画面を開けます。既存ファイルは置換前にバックアップされます。インポートしたサーバーだけ、プロパティ設定の正本が`data/server.properties`に切り替わり、設定画面もそのファイルを読み書きします。ホワイトリストとOPのメンバー一覧は引き続き`server.env`で管理します。起動中にファイルを直接編集しないでください。 + +ホーム画面からサーバーフォルダ、または永続データの`data/`をWindowsのExplorerで開けます。コマンドでは`mcserver-kit server open server`または`open data`です。WSLとExplorerの連携が必要です。 古い形式のサーバーを初めて開く場合は、移行前に確認画面を表示します。元のComposeは`compose.yaml.mcserver-kit.bak`として保存します。 diff --git a/README.md b/README.md index 6984bb6..6f7b3a2 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,9 @@ mcserver-kit server logs mcserver-kit server logs --no-follow mcserver-kit server down mcserver-kit server properties +mcserver-kit server import-properties /path/to/server.properties +mcserver-kit server open data +mcserver-kit server open server ``` `stop` and `shutdown` stop the container without removing it. `down` removes the container and network. These commands do not delete the server's `data/` directory. @@ -183,7 +186,11 @@ mcserver-kit server properties The editor covers MOTD, difficulty, game mode, player limit, online mode, whitelist, operators, flight, command blocks, PvP, view and simulation distance, spawn protection, Nether and entity spawning, and resource packs. -`server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts. +Normally, `server.env` is the source of truth for settings managed by the toolkit. Docker Compose passes these values to `itzg/minecraft-server`, which applies them to `server.properties` when the container starts. + +For a distributed `server.properties`, stop the server and choose **Import server.properties** in the dashboard, or run `mcserver-kit server import-properties /path/to/server.properties`. On WSL, the dashboard offers a Windows file picker when Windows dialogs are enabled. The existing file is backed up before replacement. Only that server switches to using `data/server.properties` as the source of truth for property settings; the editor then reads and writes that file. Whitelist and OP member lists remain in `server.env`. Do not edit the file while the server is running. + +The dashboard can also open the server folder or its persistent `data/` folder in Windows Explorer. From a terminal, use `mcserver-kit server open server` or `open data`. This requires WSL interop with Explorer. When an older server is opened for the first time, the editor asks before migrating it. The original Compose file is saved as `compose.yaml.mcserver-kit.bak`. From e517dc59f5cdf561568152e1a7e693442fefe49c Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:58:56 +0900 Subject: [PATCH 3/4] fix: satisfy ShellCheck for Explorer availability --- libexec/mcserver-kit/server-manager.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libexec/mcserver-kit/server-manager.sh b/libexec/mcserver-kit/server-manager.sh index cb61a85..94ed1d1 100755 --- a/libexec/mcserver-kit/server-manager.sh +++ b/libexec/mcserver-kit/server-manager.sh @@ -132,7 +132,9 @@ open_folder() { *) die "$(tr server.open_usage)" ;; esac [[ -d "$target" ]] || die "$(tr server.open_missing "$target")" - command -v wslpath >/dev/null 2>&1 && command -v explorer.exe >/dev/null 2>&1 || die "$(tr server.explorer_unavailable)" + if ! command -v wslpath >/dev/null 2>&1 || ! command -v explorer.exe >/dev/null 2>&1; then + die "$(tr server.explorer_unavailable)" + fi windows_path="$(wslpath -w "$target")" || die "$(tr server.explorer_unavailable)" explorer.exe "$windows_path" } From 0edecc811c0a646f550608100744ac248e7c8ad4 Mon Sep 17 00:00:00 2001 From: cotore-game <102813037+cotore-game@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:01:26 +0900 Subject: [PATCH 4/4] test: derive installed version expectations from VERSION --- tests/run-tests.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/run-tests.sh b/tests/run-tests.sh index a132e0d..cf8b78d 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -379,7 +379,9 @@ test_local_installation() { local install_log="${temp_dir}/install/install.log" local shell_rc="${temp_dir}/install/bashrc" local update_cache="${temp_dir}/install/cache" - local installed_help + local installed_help repo_version + + repo_version="$(head -n 1 "${REPO_ROOT}/VERSION")" mkdir -p "$fake_bin" ln -s /usr/bin/true "${fake_bin}/unzip" @@ -394,7 +396,7 @@ test_local_installation() { assert_equal 'present' "$([[ -x "${bin_dir}/mcserver-kit" ]] && printf present)" 'the installer creates the launcher' installed_help="$("${bin_dir}/mcserver-kit" --help)" assert_equal 'present' "$(grep -q 'mcserver-kit setup' <<<"$installed_help" && printf present)" 'the installed launcher exposes subcommand help' - assert_equal '1.1.1' "$("${bin_dir}/mcserver-kit" --version | awk '{print $2}')" 'the installed launcher reports its version' + assert_equal "$repo_version" "$("${bin_dir}/mcserver-kit" --version | awk '{print $2}')" 'the installed launcher reports its version' assert_equal 'present' "$([[ -f "${config_dir}/config.yml" ]] && printf present)" 'the installer creates the initial config' assert_equal 'present' "$([[ -f "${install_dir}/libexec/mcserver-kit/windows-dialog.ps1" ]] && printf present)" 'the installer includes the Windows dialog helper' assert_equal 'present' "$([[ -x "${install_dir}/libexec/mcserver-kit/lang.sh" ]] && printf present)" 'the installer includes the language command' @@ -408,7 +410,7 @@ test_local_installation() { assert_equal 'en' "$(cat "${config_dir}/language")" 'the installer defaults to English' assert_equal '1' "$(grep -Fxc '# >>> mcserver-kit PATH >>>' "$shell_rc")" 'the installer registers one managed PATH block' assert_equal 'present' "$(grep -q 'mcserver-kit setup' "$install_log" && printf present)" 'the installer instructs the user to run setup' - assert_equal 'present' "$(grep -q 'mcserver-kit 1.1.1' "$install_log" && printf present)" 'the installer shows the installed version' + assert_equal 'present' "$(grep -Fq "mcserver-kit ${repo_version}" "$install_log" && printf present)" 'the installer shows the installed version' assert_equal 'absent' "$(! grep -q '初回セットアップを開始' "$install_log" && printf absent)" 'the installer does not start setup automatically' printf '\n# preserve-on-update\n' >>"${config_dir}/config.yml"