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/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/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/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",
@ -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/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/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/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",
@ -3932,24 +3932,24 @@
"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/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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/58/f5/97d56302b2320c328a1702b29b30",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/d1/3c2b8fde63213bfdfcb975d7bbaa",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/94/c49ff5a94f47d2886c8db004c2ec",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5f/c5/d328666183caf3acb4a315b175c3",
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d4/e3/0f65cb6c998aad83ac0f26075951",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e7/bc/261d8d63a33d6a0e17fb20477ffd",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/44/23/5dd042e6845fef6e3690bd4d5734",
"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/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/64/c8/5ccec4eba9397107f075cf2ab401",
"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/8c/9a/2f88355ff7e46f9a9a3457890497",
"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/94/17/757605bdea7280b1b559a20a4506",
"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/ce/1c/1d030991c481b8943746a2cbba40",
"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/74/e1/35e77d9346313158b68c8a3bc37d",
"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/9c/20/50d3a17814c6994531178d62ec0f",
"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/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/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/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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/45/0c8a4e7775c699146aea682e5504",
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4a/1d/be1767741e32fd937917748944d6"
"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/eb/aa/536569d7cf92e2ef88ecedcb68e9",
"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/69/68/edb0797f52cf1684d53c2e84d449"
}

View File

@ -8,14 +8,14 @@ import time
from typing import TYPE_CHECKING
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,
ShutdownCommand, ShutdownReason,
ChatMessageCommand, ScreenMessageCommand,
ClientListCommand, KickCommand)
import _ba
from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
if TYPE_CHECKING:
from typing import Optional, Dict, Any, Type

View File

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

View File

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

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import time
@ -33,7 +34,7 @@ if TYPE_CHECKING:
# Not sure how much versioning we'll do with this, but this will get
# printed at startup in case we need it.
VERSION_STR = '1.0.2'
VERSION_STR = '1.1.0'
class ServerManagerApp:
@ -48,15 +49,26 @@ class ServerManagerApp:
self._config = self._load_config()
except Exception as exc:
raise CleanError(f'Error loading config: {exc}') from exc
self._shutdown_desired = False
self._done = False
self._process_commands: List[Union[str, ServerCommand]] = []
self._process_commands_lock = Lock()
self._restart_minutes: Optional[float] = 360.0
self._subprocess_commands: List[Union[str, ServerCommand]] = []
self._subprocess_commands_lock = Lock()
self._restart_minutes: Optional[float] = None
self._running_interactive = False
self._process: Optional[subprocess.Popen[bytes]] = None
self._process_launch_time: Optional[float] = None
self._process_sent_auto_restart = False
self._process_thread: Optional[Thread] = None
self._subprocess: Optional[subprocess.Popen[bytes]] = None
self._launch_time = time.time()
self._subprocess_launch_time: Optional[float] = 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
def config(self) -> ServerConfig:
@ -80,7 +92,6 @@ class ServerManagerApp:
def run_interactive(self) -> None:
"""Run the app loop to completion."""
import code
import signal
if self._running_interactive:
raise RuntimeError('Already running interactively.')
@ -106,8 +117,8 @@ class ServerManagerApp:
signal.signal(signal.SIGTERM, self._handle_term_signal)
# Fire off a background thread to wrangle our server binaries.
self._process_thread = Thread(target=self._bg_thread_main)
self._process_thread.start()
self._subprocess_thread = Thread(target=self._bg_thread_main)
self._subprocess_thread.start()
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.
self._done = True
self._process_thread.join()
self._subprocess_thread.join()
def cmd(self, statement: str) -> None:
"""Exec a Python command on the current running server child-process.
@ -148,8 +159,8 @@ class ServerManagerApp:
"""
if not isinstance(statement, str):
raise TypeError(f'Expected a string arg; got {type(statement)}')
with self._process_commands_lock:
self._process_commands.append(statement)
with self._subprocess_commands_lock:
self._subprocess_commands.append(statement)
self._block_for_command_completion()
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'
# interface for proper blocking two way communication.
while True:
with self._process_commands_lock:
if not self._process_commands:
with self._subprocess_commands_lock:
if not self._subprocess_commands:
break
time.sleep(0.1)
@ -225,6 +236,20 @@ class ServerManagerApp:
ShutdownCommand(reason=ShutdownReason.RESTARTING,
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:
user_config_path = 'config.yaml'
@ -266,11 +291,11 @@ class ServerManagerApp:
def _run_server_cycle(self) -> None:
"""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;
# 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
# 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}')
binary_name = ('ballisticacore_headless.exe'
if os.name == 'nt' else './ballisticacore_headless')
self._process = subprocess.Popen([binary_name, '-cfgdir', 'ba_root'],
stdin=subprocess.PIPE,
cwd='dist')
self._subprocess = subprocess.Popen(
[binary_name, '-cfgdir', 'ba_root'],
stdin=subprocess.PIPE,
cwd='dist')
# Do the thing.
# No matter how this ends up, make sure the process is dead after.
try:
self._run_process_until_exit()
self._run_subprocess_until_exit()
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."""
os.makedirs('dist/ba_root', exist_ok=True)
if os.path.exists('dist/ba_root/config.json'):
@ -313,8 +346,8 @@ class ServerManagerApp:
Can be called from any thread.
"""
with self._process_commands_lock:
self._process_commands.append(command)
with self._subprocess_commands_lock:
self._subprocess_commands.append(command)
def _send_server_command(self, command: ServerCommand) -> None:
"""Send a command to the server.
@ -322,19 +355,20 @@ class ServerManagerApp:
Must be called from the server process thread.
"""
import pickle
assert current_thread() is self._process_thread
assert self._process is not None
assert self._process.stdin is not None
assert current_thread() is self._subprocess_thread
assert self._subprocess is not None
assert self._subprocess.stdin is not None
val = repr(pickle.dumps(command))
assert '\n' not in val
execcode = (f'import ba._servermode;'
f' ba._servermode._cmd({val})\n').encode()
self._process.stdin.write(execcode)
self._process.stdin.flush()
self._subprocess.stdin.write(execcode)
self._subprocess.stdin.flush()
def _run_process_until_exit(self) -> None:
assert self._process is not None
assert self._process.stdin is not None
def _run_subprocess_until_exit(self) -> None:
assert self._subprocess 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.
# (but make sure its values are still valid first)
@ -348,31 +382,41 @@ class ServerManagerApp:
break
# Pass along any commands to our process.
with self._process_commands_lock:
for incmd in self._process_commands:
with self._subprocess_commands_lock:
for incmd in self._subprocess_commands:
# If we're passing a raw string to exec, no need to wrap it
# in any proper structure.
if isinstance(incmd, str):
self._process.stdin.write((incmd + '\n').encode())
self._process.stdin.flush()
self._subprocess.stdin.write((incmd + '\n').encode())
self._subprocess.stdin.flush()
else:
self._send_server_command(incmd)
self._process_commands = []
self._subprocess_commands = []
# Request a soft restart after a while.
assert self._process_launch_time is not None
sincelaunch = time.time() - self._process_launch_time
# Request restarts/shut-downs for various reasons.
sincelaunch = time.time() - self._subprocess_launch_time
if (self._restart_minutes is not None and sincelaunch >
(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})'
f' elapsed; requesting child-process'
f' soft restart...{Clr.RST}')
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.
code: Optional[int] = self._process.poll()
# Watch for the process exiting..
code: Optional[int] = self._subprocess.poll()
if code is not None:
if code == 0:
clr = Clr.CYN
@ -382,41 +426,44 @@ class ServerManagerApp:
slp = 5.0 # Avoid super fast death loops.
print(f'{clr}Server child-process exited'
f' with code {code}.{Clr.RST}')
self._reset_process_vars()
self._reset_subprocess_vars()
time.sleep(slp)
break
time.sleep(0.25)
def _reset_process_vars(self) -> None:
self._process = None
self._process_launch_time = None
self._process_sent_auto_restart = False
def _reset_subprocess_vars(self) -> None:
self._subprocess = None
self._subprocess_launch_time = None
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."""
assert current_thread() is self._process_thread
if self._process is None:
assert current_thread() is self._subprocess_thread
if self._subprocess is None:
return
print(f'{Clr.CYN}Stopping server process...{Clr.RST}')
# First, ask it nicely to die and give it a moment.
# If that doesn't work, bring down the hammer.
self._process.terminate()
self._subprocess.terminate()
try:
self._process.wait(timeout=10)
self._subprocess.wait(timeout=10)
except subprocess.TimeoutExpired:
self._process.kill()
self._reset_process_vars()
self._subprocess.kill()
self._reset_subprocess_vars()
print(f'{Clr.CYN}Server process stopped.{Clr.RST}')
def main() -> None:
"""Run a BallisticaCore server manager in interactive mode."""
try:
# Change our working directory according to file's path
# so that this script can be run from anywhere.
# ServerManager expects cwd to be the server dir (containing
# 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__)))
ServerManagerApp().run_interactive()

View File

@ -1,5 +1,5 @@
<!-- 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,
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>

View File

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

View File

@ -91,6 +91,25 @@ class ServerConfig:
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
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
# 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':
# User wouldn't want to pass the default of None here.
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':
vval = ('https://mystatssite.com/'
'showstats?player=${ACCOUNT}')