mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-30 11:13:17 +08:00
Work in progress on new server config options
This commit is contained in:
parent
22bcd4a3d7
commit
9ea2989bb9
@ -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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user