Skip to content
This repository has been archived by the owner on Aug 10, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5 from aliig/serverapi
Browse files Browse the repository at this point in the history
Integrate ServerAPI
  • Loading branch information
aliig authored Dec 2, 2023
2 parents 96fc462 + 6ae4a97 commit 84fda67
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 30 deletions.
13 changes: 10 additions & 3 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ server:
map: "TheIsland_WP" # The map that the server will load
admin_password: "password2" # Password for admin privileges
timezone: "America/New_York" # The timezone your server's operating system is set to
admin_list: # List of OSS IDs for server admins, check with `whoami` or `cheat listplayers`
admin_list: # optional list of OSS IDs for server admins, check with `whoami` or `cheat listplayers`
# - "12345678901234567"
use_server_api: True # set to True to use the Ark Server API to enable plugins. https://gameservershub.com/forums/resources/ark-survival-ascended-serverapi-crossplay-supported.683/

# Launch options for the server
launch_options:
Expand Down Expand Up @@ -85,8 +86,13 @@ tasks:
mod_update:
enable: True
description: "Mod update"
interval: 6 # Frequency of mod update checks in hours
interval: 24 # Frequency of mod update checks in hours
warnings: [10, 5, 1] # Warnings before restart for mod update at these minute intervals
server_api_update:
enable: True
description: "ARK Server API update"
interval: 24 # Frequency of server API update checks in hours
warnings: [10, 5, 1] # Warnings before restart for server API update at these minute intervals


# Configuration ini file overrides for game settings
Expand All @@ -100,11 +106,12 @@ advanced:
log_level: info
sleep_time: 60 # seconds to sleep between server state checks
server_timeout: 60 # seconds to wait for server to start or stop before exiting
server_api_timeout: 1800 # seconds to wait for server API to start or stop before exiting
output_directory: "output"
log_check_rate: 2 # seconds to wait between log file checks
download_url:
steamcmd: "https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip"
vc_redist: "https://aka.ms/vs/17/release/vc_redist.x64.exe"
directx: "https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe"
AmazonRootCA1: "https://www.amazontrust.com/repository/AmazonRootCA1.cer"
r2m02: "http://crt.r2m02.amazontrust.com/r2m02.cer"
r2m02: "http://crt.r2m02.amazontrust.com/r2m02.cer"
28 changes: 3 additions & 25 deletions src/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import ctypes
import os
import platform
import shutil
import winreg

import requests

from config import CONFIG
from logger import get_logger
from shell_operations import run_shell_cmd
from steamcmd import check_and_download_steamcmd
from utils import resource_path
from utils import resource_path, download_file

logger = get_logger(__name__)

Expand Down Expand Up @@ -118,6 +115,7 @@ def install_certificates_linux():


def install_dependencies_windows():
# VS Studio
if not is_dependency_installed(
winreg.HKEY_LOCAL_MACHINE,
r"Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64",
Expand All @@ -127,6 +125,7 @@ def install_dependencies_windows():
else:
logger.debug("Visual C++ Redistributable already installed.")

# DirectX
if not is_dependency_installed(
winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\DirectX"
):
Expand Down Expand Up @@ -160,24 +159,3 @@ def install_component(url, output_file, arguments):
logger.debug(f"Temporary file {component_path} deleted.")
except OSError as e:
logger.warning(f"Could not delete temporary file {component_path}: {e}")


def download_file(url, target_path=None, return_content=False):
try:
response = requests.get(url)
response.raise_for_status()
except requests.RequestException as e:
logger.error(f"Error downloading file: {e}")
return None

if return_content:
return response.content

if not target_path:
file_name = url.split("/")[-1]
target_path = os.path.join(os.environ["TEMP"], file_name)

with open(target_path, "wb") as file:
file.write(response.content)

return target_path
46 changes: 46 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from tasks import (
CheckForArkUpdatesAndRestart,
CheckForModUpdatesAndRestart,
CheckForServerAPIUpdateAndRestart,
DestroyWildDinos,
HandleEmptyServerRestart,
PerformRoutineRestart,
Expand All @@ -30,6 +31,14 @@
)
from update import does_server_need_update, is_server_installed
from utils import wait_until
from serverapi import (
install_serverapi,
use_serverapi,
serverapi_needs_update,
is_server_api_ready,
is_server_api_running,
set_log_filenames,
)

logger = get_logger(__name__)

Expand All @@ -47,6 +56,7 @@ def __init__(self):
self.tasks: dict[str, Task] = self.initialize_tasks()
self.running = True
self.server_timeout = CONFIG["advanced"].get("server_timeout", 300)
self.server_api_timeout = CONFIG["advanced"].get("server_api_timeout", 300)
self.sleep_time = CONFIG["advanced"].get("sleep_time", 60)
self.log_check_rate = CONFIG["advanced"].get("log_check_rate", 5)
self.need_certificates = not check_certificate_windows()
Expand All @@ -62,6 +72,7 @@ def initialize_tasks(self):
"mod_update": CheckForModUpdatesAndRestart,
"restart": PerformRoutineRestart,
"stale": HandleEmptyServerRestart,
"server_api_update": CheckForServerAPIUpdateAndRestart,
}

tasks = {}
Expand All @@ -79,12 +90,47 @@ def start(self) -> bool:
if does_server_need_update():
update_server()

if use_serverapi():
if serverapi_needs_update():
install_serverapi()
set_log_filenames()

delete_mods_folder()
batch_file_path = generate_batch_file()
cmd = ["cmd", "/c", batch_file_path]
logger.debug(f"Starting Ark server with cmd: {cmd}")
run_shell_cmd(cmd, use_shell=False, use_popen=True, suppress_output=True)

if use_serverapi():
# wait for server API to launch
logger.info("Waiting for server API to start...")
_, success = wait_until(
is_server_api_running,
lambda x: x,
timeout=self.server_timeout,
sleep_interval=3,
)
if not success:
logger.error("Failed to start the Ark server API")
raise ArkServerStartError("Failed to start the Ark server API.")
else:
logger.info("Ark server API started")

# wait for server API status to be ready (often long delay for PDB dumping)
logger.info("Waiting for server API to be ready...")
_, success = wait_until(
is_server_api_ready,
lambda x: x,
timeout=self.server_api_timeout,
sleep_interval=3,
)
if not success:
logger.error("Ark server API never became ready")
raise ArkServerStartError("Ark server API never became ready")
else:
logger.info("Ark server API ready")

# wait for ark server process to start
_, success = wait_until(
is_server_running,
lambda x: x,
Expand Down
153 changes: 153 additions & 0 deletions src/serverapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import os
import zipfile
import shutil

import requests
import time

from config import CONFIG, OUTDIR
from utils import download_file
from logger import get_logger
from shell_operations import run_shell_cmd

logger = get_logger(__name__)

OWNER = "ServersHub"
REPO = "ServerAPI"
LOCAL_VERSION_FILE = os.path.join(OUTDIR, f"{OWNER}_{REPO}_timestamp.txt")
API_OUTDIR = os.path.join(
CONFIG["server"]["install_path"], "ShooterGame", "Binaries", "Win64"
)
API_LOG_OUTDIR = os.path.join(API_OUTDIR, "logs")

log_filenames = []


def _extract_zip_and_move(zip_path: str, outdir: str):
with zipfile.ZipFile(zip_path, "r") as zip_ref:
for file in zip_ref.namelist():
# Construct the full path to where the file should be extracted
destination = os.path.join(outdir, file)

# Check if the file or directory should be skipped
if (file == "config.json" and os.path.exists(destination)) or (
file.startswith("Plugins/")
and os.path.exists(os.path.join(outdir, "Plugins"))
):
logger.debug(f"Skipping {file} as it already exists.")
continue

# Extract and move the file
source = zip_ref.extract(file, outdir)
os.makedirs(os.path.dirname(destination), exist_ok=True)
shutil.move(source, destination)

logger.debug(f"Extracted and moved files from {zip_path} to {outdir}")


def _get_latest_release_info(owner: str, repo: str) -> dict:
api_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
response = requests.get(api_url)
if response.status_code == 200:
release_data = response.json()
return release_data["assets"][0] # Assuming you want the first asset
else:
# logger.error("Failed to get the latest release from GitHub.")
raise RuntimeError(
f"Failed to get the latest {owner}/{repo} release from GitHub."
)


def _download_latest_github_release(
owner: str, repo: str, local_version_file: str
) -> str | None:
asset = _get_latest_release_info(owner, repo)
download_url = asset["browser_download_url"]
zip_path = download_file(download_url, return_content=False)
if zip_path:
with open(local_version_file, "w") as file:
file.write(asset["updated_at"])
return zip_path
else:
logger.error(f"Failed to download {download_url}")
return None


def _needs_update(latest_release_info: dict, local_version_file: str) -> bool:
latest_timestamp = latest_release_info["updated_at"]
if os.path.exists(local_version_file):
with open(local_version_file, "r") as file:
local_timestamp = file.read().strip()
return latest_release_info if local_timestamp != latest_timestamp else False
else:
logger.debug(f"Local version file {local_version_file} does not exist.")
return latest_release_info


def _get_log_filenames() -> list[str]:
global last_update_time
directory = API_LOG_OUTDIR

if not os.path.exists(directory):
return []

files = [os.path.join(directory, file) for file in os.listdir(directory)]
files = [file for file in files if os.path.isfile(file)]

return files


def set_log_filenames() -> None:
global log_filenames
log_filenames = _get_log_filenames()


def is_server_api_running() -> bool:
process_name = "AsaApiLoader.exe"
cmd = f'tasklist /FI "IMAGENAME eq {process_name}"'
process = run_shell_cmd(cmd, suppress_output=True)
return process.returncode == 0 and process_name in process.stdout


def is_server_api_ready() -> bool:
files = _get_log_filenames()
files = [file for file in files if file not in log_filenames]
# Filter files by modification time (consider only newer files)
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
try:
with open(files[0], "r") as f:
contents = f.read()
if "InitGame was called" in contents:
return True
except IndexError:
pass
return False


def use_serverapi() -> bool:
return "use_server_api" in CONFIG["server"] and CONFIG["server"]["use_server_api"]


def serverapi_needs_update() -> bool:
logger.info("Checking if the Ark server API needs an update...")
res = _needs_update(
latest_release_info=_get_latest_release_info(OWNER, REPO),
local_version_file=LOCAL_VERSION_FILE,
)
if res:
logger.info(f"Latest {OWNER}/{REPO} release is newer than the local version.")
return True
else:
logger.debug(f"Latest {OWNER}/{REPO} release is already downloaded.")
return False


def install_serverapi() -> None:
zip_path = _download_latest_github_release(OWNER, REPO, LOCAL_VERSION_FILE)
_extract_zip_and_move(zip_path, API_OUTDIR)
if zip_path:
logger.info(f"Downloaded latest {OWNER}/{REPO} release to {API_OUTDIR}")
else:
logger.debug(
f"Latest {OWNER}/{REPO} release is already downloaded or failed to download."
)
9 changes: 7 additions & 2 deletions src/shell_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def run_shell_cmd(

def kill_server() -> None:
run_shell_cmd("taskkill /IM ArkAscendedServer.exe /F", suppress_output=True)
run_shell_cmd("taskkill /IM ShooterGameServer.exe /F", suppress_output=True)
run_shell_cmd("taskkill /IM AsaApiLoader.exe /F", suppress_output=True)


def get_process_id(expected_port: int) -> int | None:
Expand Down Expand Up @@ -85,7 +85,12 @@ def _server_config_option(key, format_str):
"ShooterGame",
"Binaries",
"Win64",
"ArkAscendedServer.exe",
(
"AsaApiLoader.exe"
if "use_server_api" in CONFIG["server"]
and CONFIG["server"]["use_server_api"]
else "ArkAscendedServer.exe"
),
)
question_mark_options_list = [
CONFIG["server"]["map"],
Expand Down
Loading

0 comments on commit 84fda67

Please sign in to comment.