Work in progress on new server config options

This commit is contained in:
Eric Froemling 2020-11-12 14:56:32 -08:00
parent 22bcd4a3d7
commit 9ea2989bb9
9 changed files with 169 additions and 98 deletions

View File

@ -420,7 +420,7 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/04/0a/c4f7d2794b018593ab0b2bcb07f0", "assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/04/0a/c4f7d2794b018593ab0b2bcb07f0",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/06/4d/18777c9a2eb2207a2891a2837a70", "assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/06/4d/18777c9a2eb2207a2891a2837a70",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/81/90/23ab1ecc8c55267bd904a9c05344", "assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/81/90/23ab1ecc8c55267bd904a9c05344",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/58/c6/e4221985f6a7c871c5fe9a936d11", "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/71/5f/2fc5c6d238f6e84926ecabd52e8c",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/8b/fa/719ccefcd94822218fcedb9d5038", "assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/8b/fa/719ccefcd94822218fcedb9d5038",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/c1/2b/54aeb92c709c4af443f4a9013b3d", "assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/c1/2b/54aeb92c709c4af443f4a9013b3d",
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/69/cc/f8bdd1e83162481c6bf2a78cb5e0", "assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/69/cc/f8bdd1e83162481c6bf2a78cb5e0",
@ -445,7 +445,7 @@
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/59/49/d75f2b9916541bd2be8c49a5171f", "assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/59/49/d75f2b9916541bd2be8c49a5171f",
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/44/3c/7cc06ca8d5475e1687d0ed05bdbf", "assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/44/3c/7cc06ca8d5475e1687d0ed05bdbf",
"assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/38/0e/808588bf5140deae5107b61f1df7", "assets/build/ba_data/data/languages/russian.json": "https://files.ballistica.net/cache/ba1/38/0e/808588bf5140deae5107b61f1df7",
"assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/e7/d8/ace32888249fc8b8cca0e2edb48b", "assets/build/ba_data/data/languages/serbian.json": "https://files.ballistica.net/cache/ba1/3c/fe/60ad0f2a58aa1ffb742f0e8163e4",
"assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/b7/0a/fab820b96e7aa587ee56427ecdc2", "assets/build/ba_data/data/languages/slovak.json": "https://files.ballistica.net/cache/ba1/b7/0a/fab820b96e7aa587ee56427ecdc2",
"assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/67/7b/e4c6c03f889b6e1bcf39345fa197", "assets/build/ba_data/data/languages/spanish.json": "https://files.ballistica.net/cache/ba1/67/7b/e4c6c03f889b6e1bcf39345fa197",
"assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/50/9f/be006ba19be6a69a57837eb6dca0", "assets/build/ba_data/data/languages/swedish.json": "https://files.ballistica.net/cache/ba1/50/9f/be006ba19be6a69a57837eb6dca0",
@ -3932,24 +3932,24 @@
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450",
"assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e",
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d2/35/5ff7dd7a25041c0784ba74b40137", "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/03/b2/f474954f36b60e6c630d37816a29",
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/58/f5/97d56302b2320c328a1702b29b30", "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8c/9a/2f88355ff7e46f9a9a3457890497",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bd/95/c2eee0447fd64a41a1f36e31e257", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e6/66/9b7795c8d90d9d7815490bfb1574",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/d1/3c2b8fde63213bfdfcb975d7bbaa", "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/94/17/757605bdea7280b1b559a20a4506",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/94/c49ff5a94f47d2886c8db004c2ec", "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b7/9a/300d3ed36f3f804064698e97884a",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5f/c5/d328666183caf3acb4a315b175c3", "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ce/1c/1d030991c481b8943746a2cbba40",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b9/87/ac5d264cafb87348ad2e791a3393", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8c/93/01bcf74f104fbcc681efaa2d4fa4",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d4/e3/0f65cb6c998aad83ac0f26075951", "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/74/e1/35e77d9346313158b68c8a3bc37d",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e7/bc/261d8d63a33d6a0e17fb20477ffd", "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/60/46/646603dcf25085aeb2809401dd1c",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/44/23/5dd042e6845fef6e3690bd4d5734", "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9c/20/50d3a17814c6994531178d62ec0f",
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/ea/c6/cdc48f95725a16297b222de3c69e", "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/47/8d/35b1f14712c5fb48d9f7b3467898",
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/64/c8/5ccec4eba9397107f075cf2ab401", "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/57/18/dc3024133e4f01fb52682ba3ad6a",
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4f/4c/8590730e5d1cdae456c1b734a2a1", "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4f/4c/8590730e5d1cdae456c1b734a2a1",
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/e5/d1c3162e114e51a5b5b826c2ec7c", "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/e5/d1c3162e114e51a5b5b826c2ec7c",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ee/21/8fab3da6b974cf323024d076b609", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ee/21/8fab3da6b974cf323024d076b609",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/5d/881676243c5f44bdca677497b4d4", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/5d/881676243c5f44bdca677497b4d4",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/af/fd/b522dcf75713cd293743cff84613", "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/94/0b/86965867c6e7d5f984464456240f",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/45/0c8a4e7775c699146aea682e5504", "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/eb/aa/536569d7cf92e2ef88ecedcb68e9",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/09/31/4cf5f0cd0c180f9ecbb973b07ed9", "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e5/54/9bedbacbc1a98f2b3d945a543614",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4a/1d/be1767741e32fd937917748944d6" "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/69/68/edb0797f52cf1684d53c2e84d449"
} }

View File

@ -8,14 +8,14 @@ import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.terminal import Clr from efro.terminal import Clr
from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
from bacommon.servermanager import (ServerCommand, StartServerModeCommand, from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
ShutdownCommand, ShutdownReason, ShutdownCommand, ShutdownReason,
ChatMessageCommand, ScreenMessageCommand, ChatMessageCommand, ScreenMessageCommand,
ClientListCommand, KickCommand) ClientListCommand, KickCommand)
import _ba import _ba
from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Optional, Dict, Any, Type from typing import Optional, Dict, Any, Type

View File

@ -1,6 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Provides UI for editing a game in a playlist.""" """Provides UI for editing a game config."""
from __future__ import annotations from __future__ import annotations
@ -16,10 +16,10 @@ if TYPE_CHECKING:
class PlaylistEditGameWindow(ba.Window): class PlaylistEditGameWindow(ba.Window):
"""Window for editing a game in a playlist.""" """Window for editing a game config."""
def __init__(self, def __init__(self,
gameclass: Type[ba.GameActivity], gametype: Type[ba.GameActivity],
sessiontype: Type[ba.Session], sessiontype: Type[ba.Session],
config: Optional[Dict[str, Any]], config: Optional[Dict[str, Any]],
completion_call: Callable[[Optional[Dict[str, Any]]], Any], completion_call: Callable[[Optional[Dict[str, Any]]], Any],
@ -31,7 +31,7 @@ class PlaylistEditGameWindow(ba.Window):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
from ba.internal import (get_unowned_maps, get_filtered_map_name, from ba.internal import (get_unowned_maps, get_filtered_map_name,
get_map_class, get_map_display_string) get_map_class, get_map_display_string)
self._gameclass = gameclass self._gametype = gametype
self._sessiontype = sessiontype self._sessiontype = sessiontype
# If we're within an editing session we get passed edit_info # If we're within an editing session we get passed edit_info
@ -49,12 +49,12 @@ class PlaylistEditGameWindow(ba.Window):
self._r = 'gameSettingsWindow' self._r = 'gameSettingsWindow'
valid_maps = gameclass.get_supported_maps(sessiontype) valid_maps = gametype.get_supported_maps(sessiontype)
if not valid_maps: if not valid_maps:
ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText')) ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText'))
raise Exception('No valid maps') raise Exception('No valid maps')
self._settings_defs = gameclass.get_available_settings(sessiontype) self._settings_defs = gametype.get_available_settings(sessiontype)
self._completion_call = completion_call self._completion_call = completion_call
# To start with, pick a random map out of the ones we own. # To start with, pick a random map out of the ones we own.
@ -140,7 +140,7 @@ class PlaylistEditGameWindow(ba.Window):
ba.textwidget(parent=self._root_widget, ba.textwidget(parent=self._root_widget,
position=(-8, height - 70 + y_extra2), position=(-8, height - 70 + y_extra2),
size=(width, 25), size=(width, 25),
text=gameclass.get_display_string(), text=gametype.get_display_string(),
color=ba.app.ui.title_color, color=ba.app.ui.title_color,
maxwidth=235, maxwidth=235,
scale=1.1, scale=1.1,
@ -241,7 +241,6 @@ class PlaylistEditGameWindow(ba.Window):
# Handle types with choices specially: # Handle types with choices specially:
if isinstance(setting, ba.ChoiceSetting): if isinstance(setting, ba.ChoiceSetting):
# if 'choices' in setting:
for choice in setting.choices: for choice in setting.choices:
if len(choice) != 2: if len(choice) != 2:
raise ValueError( raise ValueError(
@ -429,7 +428,7 @@ class PlaylistEditGameWindow(ba.Window):
# Replace ourself with the map-select UI. # Replace ourself with the map-select UI.
ba.containerwidget(edit=self._root_widget, transition='out_left') ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window( ba.app.ui.set_main_menu_window(
PlaylistMapSelectWindow(self._gameclass, self._sessiontype, PlaylistMapSelectWindow(self._gametype, self._sessiontype,
copy.deepcopy(self._getconfig()), copy.deepcopy(self._getconfig()),
self._edit_info, self._edit_info,
self._completion_call).get_root_widget()) self._completion_call).get_root_widget())

View File

@ -18,14 +18,14 @@ class PlaylistMapSelectWindow(ba.Window):
"""Window to select a map.""" """Window to select a map."""
def __init__(self, def __init__(self,
gameclass: Type[ba.GameActivity], gametype: Type[ba.GameActivity],
sessiontype: Type[ba.Session], sessiontype: Type[ba.Session],
config: Dict[str, Any], config: Dict[str, Any],
edit_info: Dict[str, Any], edit_info: Dict[str, Any],
completion_call: Callable[[Optional[Dict[str, Any]]], Any], completion_call: Callable[[Optional[Dict[str, Any]]], Any],
transition: str = 'in_right'): transition: str = 'in_right'):
from ba.internal import get_filtered_map_name from ba.internal import get_filtered_map_name
self._gameclass = gameclass self._gametype = gametype
self._sessiontype = sessiontype self._sessiontype = sessiontype
self._config = config self._config = config
self._completion_call = completion_call self._completion_call = completion_call
@ -69,7 +69,7 @@ class PlaylistMapSelectWindow(ba.Window):
scale=1.1, scale=1.1,
text=ba.Lstr(resource='mapSelectTitleText', text=ba.Lstr(resource='mapSelectTitleText',
subs=[('${GAME}', subs=[('${GAME}',
self._gameclass.get_display_string()) self._gametype.get_display_string())
]), ]),
color=ba.app.ui.title_color, color=ba.app.ui.title_color,
h_align='center', h_align='center',
@ -104,7 +104,7 @@ class PlaylistMapSelectWindow(ba.Window):
model_transparent = ba.getmodel('level_select_button_transparent') model_transparent = ba.getmodel('level_select_button_transparent')
self._maps = [] self._maps = []
map_list = self._gameclass.get_supported_maps(self._sessiontype) map_list = self._gametype.get_supported_maps(self._sessiontype)
map_list_sorted = list(map_list) map_list_sorted = list(map_list)
map_list_sorted.sort() map_list_sorted.sort()
unowned_maps = get_unowned_maps() unowned_maps = get_unowned_maps()
@ -227,7 +227,7 @@ class PlaylistMapSelectWindow(ba.Window):
ba.containerwidget(edit=self._root_widget, transition='out_right') ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window( ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow( PlaylistEditGameWindow(
self._gameclass, self._gametype,
self._sessiontype, self._sessiontype,
self._config, self._config,
self._completion_call, self._completion_call,
@ -247,7 +247,7 @@ class PlaylistMapSelectWindow(ba.Window):
ba.containerwidget(edit=self._root_widget, transition='out_right') ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window( ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow( PlaylistEditGameWindow(
self._gameclass, self._gametype,
self._sessiontype, self._sessiontype,
self._config, self._config,
self._completion_call, self._completion_call,

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import json import json
import os import os
import signal
import subprocess import subprocess
import sys import sys
import time import time
@ -33,7 +34,7 @@ if TYPE_CHECKING:
# Not sure how much versioning we'll do with this, but this will get # Not sure how much versioning we'll do with this, but this will get
# printed at startup in case we need it. # printed at startup in case we need it.
VERSION_STR = '1.0.2' VERSION_STR = '1.1.0'
class ServerManagerApp: class ServerManagerApp:
@ -48,15 +49,26 @@ class ServerManagerApp:
self._config = self._load_config() self._config = self._load_config()
except Exception as exc: except Exception as exc:
raise CleanError(f'Error loading config: {exc}') from exc raise CleanError(f'Error loading config: {exc}') from exc
self._shutdown_desired = False
self._done = False self._done = False
self._process_commands: List[Union[str, ServerCommand]] = [] self._subprocess_commands: List[Union[str, ServerCommand]] = []
self._process_commands_lock = Lock() self._subprocess_commands_lock = Lock()
self._restart_minutes: Optional[float] = 360.0 self._restart_minutes: Optional[float] = None
self._running_interactive = False self._running_interactive = False
self._process: Optional[subprocess.Popen[bytes]] = None self._subprocess: Optional[subprocess.Popen[bytes]] = None
self._process_launch_time: Optional[float] = None self._launch_time = time.time()
self._process_sent_auto_restart = False self._subprocess_launch_time: Optional[float] = None
self._process_thread: Optional[Thread] = None self._subprocess_sent_auto_restart = False
self._subprocess_sent_unclean_exit = False
self._subprocess_thread: Optional[Thread] = None
# If we don't have any explicit exit conditions set,
# we run indefinitely (though we restart our subprocess
# periodically to clear out leaks/cruft)
if (self._config.clean_exit_minutes is None
and self._config.unclean_exit_minutes is None
and self._config.idle_exit_minutes is None):
self._restart_minutes = 360.0
@property @property
def config(self) -> ServerConfig: def config(self) -> ServerConfig:
@ -80,7 +92,6 @@ class ServerManagerApp:
def run_interactive(self) -> None: def run_interactive(self) -> None:
"""Run the app loop to completion.""" """Run the app loop to completion."""
import code import code
import signal
if self._running_interactive: if self._running_interactive:
raise RuntimeError('Already running interactively.') raise RuntimeError('Already running interactively.')
@ -106,8 +117,8 @@ class ServerManagerApp:
signal.signal(signal.SIGTERM, self._handle_term_signal) signal.signal(signal.SIGTERM, self._handle_term_signal)
# Fire off a background thread to wrangle our server binaries. # Fire off a background thread to wrangle our server binaries.
self._process_thread = Thread(target=self._bg_thread_main) self._subprocess_thread = Thread(target=self._bg_thread_main)
self._process_thread.start() self._subprocess_thread.start()
context = {'__name__': '__console__', '__doc__': None, 'mgr': self} context = {'__name__': '__console__', '__doc__': None, 'mgr': self}
@ -138,7 +149,7 @@ class ServerManagerApp:
# Mark ourselves as shutting down and wait for the process to wrap up. # Mark ourselves as shutting down and wait for the process to wrap up.
self._done = True self._done = True
self._process_thread.join() self._subprocess_thread.join()
def cmd(self, statement: str) -> None: def cmd(self, statement: str) -> None:
"""Exec a Python command on the current running server child-process. """Exec a Python command on the current running server child-process.
@ -148,8 +159,8 @@ class ServerManagerApp:
""" """
if not isinstance(statement, str): if not isinstance(statement, str):
raise TypeError(f'Expected a string arg; got {type(statement)}') raise TypeError(f'Expected a string arg; got {type(statement)}')
with self._process_commands_lock: with self._subprocess_commands_lock:
self._process_commands.append(statement) self._subprocess_commands.append(statement)
self._block_for_command_completion() self._block_for_command_completion()
def _block_for_command_completion(self) -> None: def _block_for_command_completion(self) -> None:
@ -159,8 +170,8 @@ class ServerManagerApp:
# it. In the future we can perhaps add a proper 'command port' # it. In the future we can perhaps add a proper 'command port'
# interface for proper blocking two way communication. # interface for proper blocking two way communication.
while True: while True:
with self._process_commands_lock: with self._subprocess_commands_lock:
if not self._process_commands: if not self._subprocess_commands:
break break
time.sleep(0.1) time.sleep(0.1)
@ -225,6 +236,20 @@ class ServerManagerApp:
ShutdownCommand(reason=ShutdownReason.RESTARTING, ShutdownCommand(reason=ShutdownReason.RESTARTING,
immediate=immediate)) immediate=immediate))
def shutdown(self, immediate: bool = False) -> None:
"""Shut down the server child-process and exit the wrapper
By default, the server will exit at the next good transition
point (end of a series, etc) but passing immediate=True will stop
it immediately.
"""
from bacommon.servermanager import ShutdownCommand, ShutdownReason
self._enqueue_server_command(
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate))
# So we know to bail completely once this subprocess completes.
self._shutdown_desired = True
def _load_config(self) -> ServerConfig: def _load_config(self) -> ServerConfig:
user_config_path = 'config.yaml' user_config_path = 'config.yaml'
@ -266,11 +291,11 @@ class ServerManagerApp:
def _run_server_cycle(self) -> None: def _run_server_cycle(self) -> None:
"""Spin up the server child-process and run it until exit.""" """Spin up the server child-process and run it until exit."""
self._prep_process_environment() self._prep_subprocess_environment()
# Launch the binary and grab its stdin; # Launch the binary and grab its stdin;
# we'll use this to feed it commands. # we'll use this to feed it commands.
self._process_launch_time = time.time() self._subprocess_launch_time = time.time()
# Set an environment var so the server process knows its being # Set an environment var so the server process knows its being
# run under us. This causes it to ignore ctrl-c presses and other # run under us. This causes it to ignore ctrl-c presses and other
@ -280,18 +305,26 @@ class ServerManagerApp:
print(f'{Clr.CYN}Launching server child-process...{Clr.RST}') print(f'{Clr.CYN}Launching server child-process...{Clr.RST}')
binary_name = ('ballisticacore_headless.exe' binary_name = ('ballisticacore_headless.exe'
if os.name == 'nt' else './ballisticacore_headless') if os.name == 'nt' else './ballisticacore_headless')
self._process = subprocess.Popen([binary_name, '-cfgdir', 'ba_root'], self._subprocess = subprocess.Popen(
stdin=subprocess.PIPE, [binary_name, '-cfgdir', 'ba_root'],
cwd='dist') stdin=subprocess.PIPE,
cwd='dist')
# Do the thing. # Do the thing.
# No matter how this ends up, make sure the process is dead after. # No matter how this ends up, make sure the process is dead after.
try: try:
self._run_process_until_exit() self._run_subprocess_until_exit()
finally: finally:
self._kill_process() self._kill_subprocess()
def _prep_process_environment(self) -> None: if self._shutdown_desired:
self._done = True
# Our main thread will still be blocked in its prompt or whatnot;
# let it know it should die.
os.kill(os.getpid(), signal.SIGTERM)
def _prep_subprocess_environment(self) -> None:
"""Write files that must exist at process launch.""" """Write files that must exist at process launch."""
os.makedirs('dist/ba_root', exist_ok=True) os.makedirs('dist/ba_root', exist_ok=True)
if os.path.exists('dist/ba_root/config.json'): if os.path.exists('dist/ba_root/config.json'):
@ -313,8 +346,8 @@ class ServerManagerApp:
Can be called from any thread. Can be called from any thread.
""" """
with self._process_commands_lock: with self._subprocess_commands_lock:
self._process_commands.append(command) self._subprocess_commands.append(command)
def _send_server_command(self, command: ServerCommand) -> None: def _send_server_command(self, command: ServerCommand) -> None:
"""Send a command to the server. """Send a command to the server.
@ -322,19 +355,20 @@ class ServerManagerApp:
Must be called from the server process thread. Must be called from the server process thread.
""" """
import pickle import pickle
assert current_thread() is self._process_thread assert current_thread() is self._subprocess_thread
assert self._process is not None assert self._subprocess is not None
assert self._process.stdin is not None assert self._subprocess.stdin is not None
val = repr(pickle.dumps(command)) val = repr(pickle.dumps(command))
assert '\n' not in val assert '\n' not in val
execcode = (f'import ba._servermode;' execcode = (f'import ba._servermode;'
f' ba._servermode._cmd({val})\n').encode() f' ba._servermode._cmd({val})\n').encode()
self._process.stdin.write(execcode) self._subprocess.stdin.write(execcode)
self._process.stdin.flush() self._subprocess.stdin.flush()
def _run_process_until_exit(self) -> None: def _run_subprocess_until_exit(self) -> None:
assert self._process is not None assert self._subprocess is not None
assert self._process.stdin is not None assert self._subprocess.stdin is not None
assert self._subprocess_launch_time is not None
# Send the initial server config which should kick things off. # Send the initial server config which should kick things off.
# (but make sure its values are still valid first) # (but make sure its values are still valid first)
@ -348,31 +382,41 @@ class ServerManagerApp:
break break
# Pass along any commands to our process. # Pass along any commands to our process.
with self._process_commands_lock: with self._subprocess_commands_lock:
for incmd in self._process_commands: for incmd in self._subprocess_commands:
# If we're passing a raw string to exec, no need to wrap it # If we're passing a raw string to exec, no need to wrap it
# in any proper structure. # in any proper structure.
if isinstance(incmd, str): if isinstance(incmd, str):
self._process.stdin.write((incmd + '\n').encode()) self._subprocess.stdin.write((incmd + '\n').encode())
self._process.stdin.flush() self._subprocess.stdin.flush()
else: else:
self._send_server_command(incmd) self._send_server_command(incmd)
self._process_commands = [] self._subprocess_commands = []
# Request a soft restart after a while. # Request restarts/shut-downs for various reasons.
assert self._process_launch_time is not None sincelaunch = time.time() - self._subprocess_launch_time
sincelaunch = time.time() - self._process_launch_time
if (self._restart_minutes is not None and sincelaunch > if (self._restart_minutes is not None and sincelaunch >
(self._restart_minutes * 60.0) (self._restart_minutes * 60.0)
and not self._process_sent_auto_restart): and not self._subprocess_sent_auto_restart):
print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})' print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})'
f' elapsed; requesting child-process' f' elapsed; requesting child-process'
f' soft restart...{Clr.RST}') f' soft restart...{Clr.RST}')
self.restart() self.restart()
self._process_sent_auto_restart = True self._subprocess_sent_auto_restart = True
if self._config.unclean_exit_minutes is not None:
elapsed = (time.time() - self._launch_time) / 60.0
threshold = self._config.unclean_exit_minutes
if (elapsed > threshold
and not self._subprocess_sent_unclean_exit):
print(f'{Clr.CYN}unclean_exit_minutes'
f' ({threshold})'
f' elapsed; requesting child-process'
f' shutdown...{Clr.RST}')
self.shutdown(immediate=True)
self._subprocess_sent_unclean_exit = True
# Watch for the process exiting. # Watch for the process exiting..
code: Optional[int] = self._process.poll() code: Optional[int] = self._subprocess.poll()
if code is not None: if code is not None:
if code == 0: if code == 0:
clr = Clr.CYN clr = Clr.CYN
@ -382,41 +426,44 @@ class ServerManagerApp:
slp = 5.0 # Avoid super fast death loops. slp = 5.0 # Avoid super fast death loops.
print(f'{clr}Server child-process exited' print(f'{clr}Server child-process exited'
f' with code {code}.{Clr.RST}') f' with code {code}.{Clr.RST}')
self._reset_process_vars() self._reset_subprocess_vars()
time.sleep(slp) time.sleep(slp)
break break
time.sleep(0.25) time.sleep(0.25)
def _reset_process_vars(self) -> None: def _reset_subprocess_vars(self) -> None:
self._process = None self._subprocess = None
self._process_launch_time = None self._subprocess_launch_time = None
self._process_sent_auto_restart = False self._subprocess_sent_auto_restart = False
self._subprocess_sent_unclean_exit = False
def _kill_process(self) -> None: def _kill_subprocess(self) -> None:
"""End the server process if it still exists.""" """End the server process if it still exists."""
assert current_thread() is self._process_thread assert current_thread() is self._subprocess_thread
if self._process is None: if self._subprocess is None:
return return
print(f'{Clr.CYN}Stopping server process...{Clr.RST}') print(f'{Clr.CYN}Stopping server process...{Clr.RST}')
# First, ask it nicely to die and give it a moment. # First, ask it nicely to die and give it a moment.
# If that doesn't work, bring down the hammer. # If that doesn't work, bring down the hammer.
self._process.terminate() self._subprocess.terminate()
try: try:
self._process.wait(timeout=10) self._subprocess.wait(timeout=10)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self._process.kill() self._subprocess.kill()
self._reset_process_vars() self._reset_subprocess_vars()
print(f'{Clr.CYN}Server process stopped.{Clr.RST}') print(f'{Clr.CYN}Server process stopped.{Clr.RST}')
def main() -> None: def main() -> None:
"""Run a BallisticaCore server manager in interactive mode.""" """Run a BallisticaCore server manager in interactive mode."""
try: try:
# Change our working directory according to file's path # ServerManager expects cwd to be the server dir (containing
# so that this script can be run from anywhere. # dist/, config.yaml, etc.)
# Let's change our working directory to the location of this file
# so we can run this script from anywhere and it'll work.
os.chdir(os.path.abspath(os.path.dirname(__file__))) os.chdir(os.path.abspath(os.path.dirname(__file__)))
ServerManagerApp().run_interactive() ServerManagerApp().run_interactive()

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-11-11 for Ballistica version 1.5.28 build 20246</em></h4> <h4><em>last updated on 2020-11-12 for Ballistica version 1.5.29 build 20248</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p> which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr> <hr>

View File

@ -21,7 +21,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't change here. // These are set automatically via script; don't change here.
const int kAppBuildNumber = 20247; const int kAppBuildNumber = 20248;
const char* kAppVersion = "1.5.29"; const char* kAppVersion = "1.5.29";
// Our standalone globals. // Our standalone globals.

View File

@ -91,6 +91,25 @@ class ServerConfig:
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE # http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
stats_url: Optional[str] = None stats_url: Optional[str] = None
# If present, the server will attempt to gracefully exit after this
# amount of time. A graceful exit can occur at the end of a series
# or other opportune time.
# Servers with no exit times set will run indefinitely (though the server
# binary will be restarted periodically to clear any leaked memory).
clean_exit_minutes: Optional[float] = None
# If present, the server will shut down immediately after the given
# amount of time). This can be useful as a fallback for clean_exit_time.
# Servers with no exit times set will run indefinitely (though the server
# binary will be restarted periodically to clear any leaked memory).
unclean_exit_minutes: Optional[float] = None
# If present, the server will shut down immediately if this amount of
# time passes with no connected clients.
# Servers with no exit times set will run indefinitely (though the server
# binary will be restarted periodically to clear any leaked memory).
idle_exit_minutes: Optional[float] = None
# NOTE: as much as possible, communication from the server-manager to the # NOTE: as much as possible, communication from the server-manager to the
# child-process should go through these and not ad-hoc Python string commands # child-process should go through these and not ad-hoc Python string commands

View File

@ -642,6 +642,12 @@ def _get_server_config_template_yaml(projroot: str) -> str:
if vname == 'playlist_code': if vname == 'playlist_code':
# User wouldn't want to pass the default of None here. # User wouldn't want to pass the default of None here.
vval = 12345 vval = 12345
elif vname == 'clean_exit_time':
vval = 60
elif vname == 'unclean_exit_time':
vval = 90
elif vname == 'idle_exit_time':
vval = 20
elif vname == 'stats_url': elif vname == 'stats_url':
vval = ('https://mystatssite.com/' vval = ('https://mystatssite.com/'
'showstats?player=${ACCOUNT}') 'showstats?player=${ACCOUNT}')