mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-24 07:53:30 +08:00
Wired up graceful server restarts and exits
This commit is contained in:
parent
f6566bae27
commit
2cdea7f392
@ -420,8 +420,8 @@
|
||||
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/75/1d/868bb04cf691736035c917d02762",
|
||||
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/44/2a/8535b446284235cb503947ece074",
|
||||
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/f5/d3/8e941851c4310465646c4167afc1",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/57/71/1a5fdd872252e5f21c3d3f7f4e29",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/d1/bd/924adf7035ab6da8b2589f71b909",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/73/be/02615076b8cf6a6fb052cf26ba7e",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/a7/87/0cf7ff45e545742202d882f0eefc",
|
||||
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/49/5f/b29bb65369040892fe6601801637",
|
||||
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/53/2e/850d71d7ed654a3741527a31dc8d",
|
||||
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/aa/91/2411c0728bae33619c21237a2689",
|
||||
@ -429,11 +429,11 @@
|
||||
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/f2/90/62968ad28a2499a8d182a5740a85",
|
||||
"assets/build/ba_data/data/languages/danish.json": "https://files.ballistica.net/cache/ba1/3f/46/e4da3c1d2b0ebf916df55c608b28",
|
||||
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/86/33/8400929a710ae4a90f3f7cb57518",
|
||||
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/41/be/aae8073e1bfc8aa378f8708d4a96",
|
||||
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/a2/ef/1c2207471cb58efc805115b31468",
|
||||
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/6e/fd/685a4e1da031474d47a1d9eb2731",
|
||||
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/b4/35/4860ac0f2f30881221b5545560ce",
|
||||
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/9d/00/a8c4ef9f0a25e789c046bd741203",
|
||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/79/ac/38415932002fffdfe22639fd87f1",
|
||||
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/8f/6a/32ef6c0141abace80b812c0b334b",
|
||||
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/17/78/3fd0dca40e632ce53d03a944e7fa",
|
||||
"assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/7a/64/04464dc6ee8a45632857fa436bff",
|
||||
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/4d/4b/0790110201c9adb1b521e9a55e63",
|
||||
|
||||
6
.idea/dictionaries/ericf.xml
generated
6
.idea/dictionaries/ericf.xml
generated
@ -383,6 +383,7 @@
|
||||
<w>curhashes</w>
|
||||
<w>curstate</w>
|
||||
<w>curtime</w>
|
||||
<w>curtimestr</w>
|
||||
<w>customizebrowser</w>
|
||||
<w>cutscenes</w>
|
||||
<w>cval</w>
|
||||
@ -565,6 +566,7 @@
|
||||
<w>excludepowerups</w>
|
||||
<w>excludetypes</w>
|
||||
<w>excstr</w>
|
||||
<w>execcode</w>
|
||||
<w>execlocals</w>
|
||||
<w>executils</w>
|
||||
<w>exhash</w>
|
||||
@ -1323,6 +1325,7 @@
|
||||
<w>playtypes</w>
|
||||
<w>plines</w>
|
||||
<w>plistlib</w>
|
||||
<w>plistname</w>
|
||||
<w>plpt</w>
|
||||
<w>plst</w>
|
||||
<w>plusbutton</w>
|
||||
@ -1587,6 +1590,7 @@
|
||||
<w>serverdialog</w>
|
||||
<w>serverget</w>
|
||||
<w>servermanager</w>
|
||||
<w>servermode</w>
|
||||
<w>serverput</w>
|
||||
<w>serverutils</w>
|
||||
<w>sessionclass</w>
|
||||
@ -1614,6 +1618,7 @@
|
||||
<w>shroom</w>
|
||||
<w>shutil</w>
|
||||
<w>simplesubclasses</w>
|
||||
<w>sincelaunch</w>
|
||||
<w>singledispatch</w>
|
||||
<w>sirplus</w>
|
||||
<w>sitebuiltins</w>
|
||||
@ -1827,6 +1832,7 @@
|
||||
<w>timeremaining</w>
|
||||
<w>timestep</w>
|
||||
<w>timestring</w>
|
||||
<w>timestrval</w>
|
||||
<w>timetype</w>
|
||||
<w>tipstext</w>
|
||||
<w>titletext</w>
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
"ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_server.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_servermode.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_session.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_stats.cpython-37.opt-1.pyc",
|
||||
"ba_data/python/ba/__pycache__/_store.cpython-37.opt-1.pyc",
|
||||
@ -93,7 +93,7 @@
|
||||
"ba_data/python/ba/_playlist.py",
|
||||
"ba_data/python/ba/_powerup.py",
|
||||
"ba_data/python/ba/_profile.py",
|
||||
"ba_data/python/ba/_server.py",
|
||||
"ba_data/python/ba/_servermode.py",
|
||||
"ba_data/python/ba/_session.py",
|
||||
"ba_data/python/ba/_stats.py",
|
||||
"ba_data/python/ba/_store.py",
|
||||
|
||||
@ -153,6 +153,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
||||
build/ba_data/python/ba/_coopgame.py \
|
||||
build/ba_data/python/ba/_meta.py \
|
||||
build/ba_data/python/ba/_math.py \
|
||||
build/ba_data/python/ba/_servermode.py \
|
||||
build/ba_data/python/ba/_appconfig.py \
|
||||
build/ba_data/python/ba/_gameresults.py \
|
||||
build/ba_data/python/ba/_profile.py \
|
||||
@ -190,7 +191,6 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
||||
build/ba_data/python/ba/_level.py \
|
||||
build/ba_data/python/ba/_dependency.py \
|
||||
build/ba_data/python/ba/_general.py \
|
||||
build/ba_data/python/ba/_server.py \
|
||||
build/ba_data/python/ba/_account.py \
|
||||
build/ba_data/python/ba/_music.py \
|
||||
build/ba_data/python/ba/_lang.py \
|
||||
@ -378,6 +378,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
||||
build/ba_data/python/ba/__pycache__/_coopgame.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_meta.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_math.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_servermode.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_appconfig.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_gameresults.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc \
|
||||
@ -415,7 +416,6 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
||||
build/ba_data/python/ba/__pycache__/_level.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_dependency.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_general.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_server.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc \
|
||||
build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc \
|
||||
@ -659,6 +659,11 @@ build/ba_data/python/ba/__pycache__/_math.cpython-37.opt-1.pyc: \
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/ba/__pycache__/_servermode.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/ba/_servermode.py
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/ba/__pycache__/_appconfig.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/ba/_appconfig.py
|
||||
@echo Compiling script: $^
|
||||
@ -844,11 +849,6 @@ build/ba_data/python/ba/__pycache__/_general.cpython-37.opt-1.pyc: \
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/ba/__pycache__/_server.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/ba/_server.py
|
||||
@echo Compiling script: $^
|
||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
||||
|
||||
build/ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc: \
|
||||
build/ba_data/python/ba/_account.py
|
||||
@echo Compiling script: $^
|
||||
|
||||
@ -59,7 +59,7 @@ from ba._gameresults import TeamGameResults
|
||||
from ba._lang import Lstr, setlanguage, get_valid_languages
|
||||
from ba._map import Map, getmaps
|
||||
from ba._session import Session
|
||||
from ba._server import ServerController
|
||||
from ba._servermode import ServerController
|
||||
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
|
||||
from ba._team import Team
|
||||
from ba._teamgame import TeamGameActivity
|
||||
|
||||
@ -29,29 +29,35 @@ from efro.terminal import Clr
|
||||
from ba._enums import TimeType
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from bacommon.servermanager import ServerConfig, ServerCommand
|
||||
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
|
||||
ShutdownCommand, ShutdownReason)
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, Dict, Any, Type
|
||||
import ba
|
||||
from bacommon.servermanager import ServerConfig
|
||||
|
||||
|
||||
def _cmd(command_data: bytes) -> None:
|
||||
"""Handle commands coming in from the server manager."""
|
||||
"""Handle commands coming in from our server manager parent process."""
|
||||
import pickle
|
||||
command, payload = pickle.loads(command_data)
|
||||
command = pickle.loads(command_data)
|
||||
assert isinstance(command, ServerCommand)
|
||||
|
||||
# We expect to receive a config command to kick things off.
|
||||
if command is ServerCommand.CONFIG:
|
||||
assert isinstance(payload, ServerConfig)
|
||||
if isinstance(command, StartServerModeCommand):
|
||||
assert _ba.app.server is None
|
||||
_ba.app.server = ServerController(payload)
|
||||
_ba.app.server = ServerController(command.config)
|
||||
return
|
||||
|
||||
assert _ba.app.server is not None
|
||||
print('WOULD DO OTHER SERVER COMMAND')
|
||||
if isinstance(command, ShutdownCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.shutdown(reason=command.reason,
|
||||
immediate=command.immediate)
|
||||
return
|
||||
|
||||
print(f'{Clr.SRED}ERROR: server process'
|
||||
f' got unknown command: {type(command)}{Clr.RST}')
|
||||
|
||||
|
||||
class ServerController:
|
||||
@ -68,6 +74,8 @@ class ServerController:
|
||||
self._run_server_wait_timer: Optional[ba.Timer] = None
|
||||
self._next_stuck_login_warn_time = time.time() + 10.0
|
||||
self._first_run = True
|
||||
self._shutdown_reason: Optional[ShutdownReason] = None
|
||||
self._executing_shutdown = False
|
||||
|
||||
# Make note if they want us to import a playlist;
|
||||
# we'll need to do that first if so.
|
||||
@ -76,59 +84,25 @@ class ServerController:
|
||||
self._playlist_fetch_got_response = False
|
||||
self._playlist_fetch_code = -1
|
||||
|
||||
self._config_server()
|
||||
|
||||
# Now sit around until we're signed in and then
|
||||
# kick off the server.
|
||||
# Now sit around doing any pre-launch prep such as waiting for
|
||||
# account sign-in or fetching playlists; this will kick off the
|
||||
# session once done.
|
||||
with _ba.Context('ui'):
|
||||
self._run_server_wait_timer = _ba.Timer(
|
||||
0.25,
|
||||
self._update_server_playlist_fetch,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True)
|
||||
self._run_server_wait_timer = _ba.Timer(0.25,
|
||||
self._prepare_to_serve,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True)
|
||||
|
||||
def launch_server_session(self) -> None:
|
||||
"""Kick off a host-session based on the current server config."""
|
||||
app = _ba.app
|
||||
appcfg = app.config
|
||||
|
||||
sessiontype = self._get_session_type()
|
||||
|
||||
if _ba.get_account_state() != 'signed_in':
|
||||
print('WARNING: launch_server_session() expects to run '
|
||||
'with a signed in server account')
|
||||
|
||||
if self._first_run:
|
||||
print((('BallisticaCore '
|
||||
if app.headless_build else 'BallisticaCore ') +
|
||||
str(app.version) + ' (' + str(app.build_number) +
|
||||
') entering server-mode ' + time.strftime('%c')))
|
||||
|
||||
appcfg['Show Tutorial'] = False
|
||||
|
||||
if sessiontype is FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
appcfg['Free-for-All Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
elif sessiontype is DualTeamSession:
|
||||
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
|
||||
appcfg['Team Tournament Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
def shutdown(self, reason: ShutdownReason, immediate: bool) -> None:
|
||||
"""Set the app to quit either now or at the next clean opportunity."""
|
||||
self._shutdown_reason = reason
|
||||
if immediate:
|
||||
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
|
||||
self._execute_shutdown()
|
||||
else:
|
||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
||||
|
||||
appcfg['Port'] = self._config.port
|
||||
|
||||
# Set series lengths.
|
||||
app.teams_series_length = self._config.teams_series_length
|
||||
app.ffa_series_length = self._config.ffa_series_length
|
||||
|
||||
# And here we go.
|
||||
_ba.new_host_session(sessiontype)
|
||||
|
||||
if not self._ran_access_check:
|
||||
self._run_access_check()
|
||||
self._ran_access_check = True
|
||||
print(f'{Clr.SBLU}Shutdown initiated;'
|
||||
f' server process will exit at the next clean opportunity.'
|
||||
f'{Clr.RST}')
|
||||
|
||||
def handle_transition(self) -> bool:
|
||||
"""Handle transitioning to a new ba.Session or quitting the app.
|
||||
@ -138,121 +112,33 @@ class ServerController:
|
||||
Should return True if action will be handled by us; False if the
|
||||
session should just continue on it's merry way.
|
||||
"""
|
||||
print('FIXME: fill out server handle_transition()')
|
||||
# If the app is in server mode and this activity
|
||||
# if self._allow_server_transition and _ba.app.server_config_dirty:
|
||||
# from ba import _server
|
||||
# from ba._lang import Lstr
|
||||
# from ba._general import Call
|
||||
# from ba._enums import TimeType
|
||||
# if _ba.app.server_config.get('quit', False):
|
||||
# if not self._kicked_off_server_shutdown:
|
||||
# if _ba.app.server_config.get(
|
||||
# 'quit_reason') == 'restarting':
|
||||
# # FIXME: Should add a server-screen-message call
|
||||
# # or something.
|
||||
# _ba.chat_message(
|
||||
# Lstr(resource='internal.serverRestartingText').
|
||||
# evaluate())
|
||||
# print(('Exiting for server-restart at ' +
|
||||
# time.strftime('%c')))
|
||||
# else:
|
||||
# print(('Exiting for server-shutdown at ' +
|
||||
# time.strftime('%c')))
|
||||
# with _ba.Context('ui'):
|
||||
# _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
# self._kicked_off_server_shutdown = True
|
||||
# return True
|
||||
# else:
|
||||
# if not self._kicked_off_server_restart:
|
||||
# print(('Running updated server config at ' +
|
||||
# time.strftime('%c')))
|
||||
# with _ba.Context('ui'):
|
||||
# _ba.timer(1.0,
|
||||
# Call(_ba.pushcall,
|
||||
# _server.launch_server_session),
|
||||
# timetype=TimeType.REAL)
|
||||
# self._kicked_off_server_restart = True
|
||||
# return True
|
||||
if self._shutdown_reason is not None:
|
||||
self._execute_shutdown()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_session_type(self) -> Type[ba.Session]:
|
||||
|
||||
# Convert string session type to the class.
|
||||
# Hmm should we just keep this as a string?
|
||||
if self._config.session_type == 'ffa':
|
||||
return FreeForAllSession
|
||||
if self._config.session_type == 'teams':
|
||||
return DualTeamSession
|
||||
raise RuntimeError(
|
||||
f'Invalid session_type: "{self._config.session_type}"')
|
||||
|
||||
def _update_server_playlist_fetch(self) -> None:
|
||||
|
||||
signed_in = _ba.get_account_state() == 'signed_in'
|
||||
|
||||
if not signed_in:
|
||||
|
||||
# Signing in to the local server account should not take long;
|
||||
# complain if it does...
|
||||
curtime = time.time()
|
||||
if curtime > self._next_stuck_login_warn_time:
|
||||
print('Still waiting for account sign-in...')
|
||||
self._next_stuck_login_warn_time = curtime + 10.0
|
||||
def _execute_shutdown(self) -> None:
|
||||
from ba._lang import Lstr
|
||||
if self._executing_shutdown:
|
||||
return
|
||||
self._executing_shutdown = True
|
||||
timestrval = time.strftime('%c')
|
||||
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||
# FIXME: Should add a server-screen-message call.
|
||||
# (so we could send this an an Lstr)
|
||||
_ba.chat_message(
|
||||
Lstr(resource='internal.serverRestartingText').evaluate())
|
||||
print(f'{Clr.SBLU}Exiting for server-restart'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
else:
|
||||
can_launch = False
|
||||
|
||||
# If we're trying to fetch a playlist, we do that first.
|
||||
if self._playlist_fetch_running:
|
||||
|
||||
# Send request if we haven't.
|
||||
if not self._playlist_fetch_sent_request:
|
||||
|
||||
print(f'Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...')
|
||||
|
||||
_ba.add_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'code': str(self._config.playlist_code),
|
||||
'overwrite': True
|
||||
},
|
||||
callback=self._on_playlist_fetch_response)
|
||||
_ba.run_transactions()
|
||||
|
||||
self._playlist_fetch_sent_request = True
|
||||
|
||||
# If we got a valid result, forget the fetch ever
|
||||
# existed and move on.
|
||||
if self._playlist_fetch_got_response:
|
||||
self._playlist_fetch_running = False
|
||||
can_launch = True
|
||||
else:
|
||||
can_launch = True
|
||||
|
||||
if can_launch:
|
||||
self._run_server_wait_timer = None
|
||||
_ba.pushcall(self.launch_server_session)
|
||||
|
||||
def _on_playlist_fetch_response(
|
||||
self,
|
||||
result: Optional[Dict[str, Any]],
|
||||
) -> None:
|
||||
if result is None:
|
||||
print('Error fetching playlist;' ' aborting.')
|
||||
sys.exit(-1)
|
||||
|
||||
# Once we get here we simply modify our
|
||||
# config to use this playlist.
|
||||
type_name = (
|
||||
'teams' if result['playlistType'] == 'Team Tournament' else
|
||||
'ffa' if result['playlistType'] == 'Free-for-All' else '??')
|
||||
print(('Playlist \'' + result['playlistName'] + '\' (' + type_name +
|
||||
') downloaded; running...'))
|
||||
|
||||
self._playlist_fetch_got_response = True
|
||||
self._config.session_type = type_name
|
||||
self._playlist_name = (result['playlistName'])
|
||||
# FIXME: Should add a server-screen-message call.
|
||||
# (so we could send this an an Lstr)
|
||||
print(f'{Clr.SBLU}Exiting for server-shutdown'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
_ba.chat_message(
|
||||
Lstr(resource='internal.serverShuttingDownText').evaluate())
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
|
||||
def _run_access_check(self) -> None:
|
||||
"""Check with the master server to see if we're likely joinable."""
|
||||
@ -272,7 +158,7 @@ class ServerController:
|
||||
print('error on UDP port access check (internet down?)')
|
||||
else:
|
||||
if data['accessible']:
|
||||
print(f'{Clr.SGRN}UDP port {gameport} access check successful.'
|
||||
print(f'{Clr.SBLU}UDP port {gameport} access check successful.'
|
||||
f' Your server appears to be joinable from the'
|
||||
f' internet.{Clr.RST}')
|
||||
else:
|
||||
@ -280,18 +166,123 @@ class ServerController:
|
||||
f' Your server does not appear to be joinable'
|
||||
f' from the internet.{Clr.RST}')
|
||||
|
||||
def _config_server(self) -> None:
|
||||
"""Apply server config changes that can take effect immediately.
|
||||
def _prepare_to_serve(self) -> None:
|
||||
|
||||
(party name, etc)
|
||||
"""
|
||||
signed_in = _ba.get_account_state() == 'signed_in'
|
||||
|
||||
_ba.app.config['Auto Balance Teams'] = (
|
||||
self._config.auto_balance_teams)
|
||||
if not signed_in:
|
||||
|
||||
# Signing in to the local server account should not take long;
|
||||
# complain if it does...
|
||||
curtime = time.time()
|
||||
if curtime > self._next_stuck_login_warn_time:
|
||||
print('Still waiting for account sign-in...')
|
||||
self._next_stuck_login_warn_time = curtime + 10.0
|
||||
else:
|
||||
can_launch = False
|
||||
|
||||
# If we're trying to fetch a playlist, we do that first.
|
||||
if self._playlist_fetch_running:
|
||||
|
||||
# Send request if we haven't.
|
||||
if not self._playlist_fetch_sent_request:
|
||||
print(f'{Clr.SBLU}Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...{Clr.RST}')
|
||||
_ba.add_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'code': str(self._config.playlist_code),
|
||||
'overwrite': True
|
||||
},
|
||||
callback=self._on_playlist_fetch_response)
|
||||
_ba.run_transactions()
|
||||
self._playlist_fetch_sent_request = True
|
||||
|
||||
# If we got a valid result, forget the fetch ever
|
||||
# existed and move on.
|
||||
if self._playlist_fetch_got_response:
|
||||
self._playlist_fetch_running = False
|
||||
can_launch = True
|
||||
else:
|
||||
can_launch = True
|
||||
|
||||
if can_launch:
|
||||
self._run_server_wait_timer = None
|
||||
_ba.pushcall(self._launch_server_session)
|
||||
|
||||
def _on_playlist_fetch_response(
|
||||
self,
|
||||
result: Optional[Dict[str, Any]],
|
||||
) -> None:
|
||||
if result is None:
|
||||
print('Error fetching playlist;' ' aborting.')
|
||||
sys.exit(-1)
|
||||
|
||||
# Once we get here we simply modify our
|
||||
# config to use this playlist.
|
||||
typename = (
|
||||
'teams' if result['playlistType'] == 'Team Tournament' else
|
||||
'ffa' if result['playlistType'] == 'Free-for-All' else '??')
|
||||
plistname = result['playlistName']
|
||||
print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}')
|
||||
|
||||
self._playlist_fetch_got_response = True
|
||||
self._config.session_type = typename
|
||||
self._playlist_name = (result['playlistName'])
|
||||
|
||||
def _get_session_type(self) -> Type[ba.Session]:
|
||||
# Convert string session type to the class.
|
||||
# Hmm should we just keep this as a string?
|
||||
if self._config.session_type == 'ffa':
|
||||
return FreeForAllSession
|
||||
if self._config.session_type == 'teams':
|
||||
return DualTeamSession
|
||||
raise RuntimeError(
|
||||
f'Invalid session_type: "{self._config.session_type}"')
|
||||
|
||||
def _launch_server_session(self) -> None:
|
||||
"""Kick off a host-session based on the current server config."""
|
||||
app = _ba.app
|
||||
appcfg = app.config
|
||||
sessiontype = self._get_session_type()
|
||||
|
||||
if _ba.get_account_state() != 'signed_in':
|
||||
print('WARNING: launch_server_session() expects to run '
|
||||
'with a signed in server account')
|
||||
|
||||
if self._first_run:
|
||||
curtimestr = time.strftime('%c')
|
||||
print(f'{Clr.SBLU}BallisticaCore {app.version}'
|
||||
f' ({app.build_number})'
|
||||
f' entering server-mode {curtimestr}{Clr.RST}')
|
||||
|
||||
appcfg['Show Tutorial'] = False
|
||||
|
||||
if sessiontype is FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
appcfg['Free-for-All Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
elif sessiontype is DualTeamSession:
|
||||
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
|
||||
appcfg['Team Tournament Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
else:
|
||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
||||
|
||||
app.teams_series_length = self._config.teams_series_length
|
||||
app.ffa_series_length = self._config.ffa_series_length
|
||||
appcfg['Port'] = self._config.port
|
||||
appcfg['Auto Balance Teams'] = self._config.auto_balance_teams
|
||||
_ba.set_public_party_max_size(self._config.max_party_size)
|
||||
_ba.set_public_party_name(self._config.party_name)
|
||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||
|
||||
# Call set-enabled last (will push state to the cloud).
|
||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
||||
|
||||
# And here we go.
|
||||
_ba.new_host_session(sessiontype)
|
||||
|
||||
if not self._ran_access_check:
|
||||
self._run_access_check()
|
||||
self._ran_access_check = True
|
||||
@ -40,12 +40,12 @@ sys.path += [
|
||||
|
||||
from efro.terminal import Clr
|
||||
from efro.dataclassutils import dataclass_assign, dataclass_validate
|
||||
from bacommon.servermanager import (ServerConfig, ServerCommand,
|
||||
make_server_command)
|
||||
from bacommon.servermanager import (ServerConfig, StartServerModeCommand)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, List, Dict
|
||||
from typing import Optional, List, Dict, Union
|
||||
from types import FrameType
|
||||
from bacommon.servermanager import ServerCommand
|
||||
|
||||
|
||||
class ServerManagerApp:
|
||||
@ -58,12 +58,13 @@ class ServerManagerApp:
|
||||
def __init__(self) -> None:
|
||||
self._config = self._load_config()
|
||||
self._done = False
|
||||
self._process_commands: List[str] = []
|
||||
self._process_commands: List[Union[str, ServerCommand]] = []
|
||||
self._process_commands_lock = Lock()
|
||||
self._restart_minutes: Optional[float] = 360.0
|
||||
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
|
||||
|
||||
@property
|
||||
@ -85,6 +86,95 @@ class ServerManagerApp:
|
||||
"""
|
||||
return self._restart_minutes
|
||||
|
||||
def run_interactive(self) -> None:
|
||||
"""Run the app loop to completion."""
|
||||
import code
|
||||
import signal
|
||||
|
||||
if self._running_interactive:
|
||||
raise RuntimeError('Already running interactively.')
|
||||
self._running_interactive = True
|
||||
|
||||
# Print basic usage info in interactive mode.
|
||||
if sys.stdin.isatty():
|
||||
print(f'{Clr.SMAG}BallisticaCore server manager starting up...\n'
|
||||
f'Use the "mgr" object to make live server adjustments.\n'
|
||||
f'Type "help(mgr)" for more information.{Clr.RST}')
|
||||
|
||||
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
||||
# need to register a SIGTERM handler so we have a chance to clean
|
||||
# up our child-process when someone tells us to die. (and avoid
|
||||
# zombie processes)
|
||||
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()
|
||||
|
||||
# According to Python docs, default locals dict has __name__ set
|
||||
# to __console__ and __doc__ set to None; using that as start point.
|
||||
# https://docs.python.org/3/library/code.html
|
||||
locs = {'__name__': '__console__', '__doc__': None, 'mgr': self}
|
||||
|
||||
# Enable tab-completion if possible.
|
||||
self._enable_tab_completion(locs)
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
# TODO: make it possible to use IPython if the user has it available.
|
||||
try:
|
||||
code.interact(local=locs, banner='', exitmsg='')
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our BG thread still running.
|
||||
pass
|
||||
except BaseException as exc:
|
||||
print(f'{Clr.SRED}Unexpected interpreter exception:'
|
||||
f' {exc}{Clr.RST}')
|
||||
|
||||
# Mark ourselves as shutting down and wait for the process to wrap up.
|
||||
self._done = True
|
||||
self._process_thread.join()
|
||||
|
||||
def cmd(self, statement: str) -> None:
|
||||
"""Exec a Python command on the current running server child-process.
|
||||
|
||||
Note that commands are executed asynchronously and no status or
|
||||
return value is accessible from this manager app.
|
||||
"""
|
||||
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)
|
||||
|
||||
# Ideally we'd block here until the command was run so our prompt would
|
||||
# print after it's results. We currently don't get any response from
|
||||
# the app so the best we can do is block until our bg thread has sent
|
||||
# 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:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# One last short delay so if we come out *just* as the command is sent
|
||||
# we'll hopefully still give it enough time to process/print.
|
||||
time.sleep(0.1)
|
||||
|
||||
def restart(self, immediate: bool = False) -> None:
|
||||
"""Restart the server child-process.
|
||||
|
||||
This can be necessary for some config changes to take effect.
|
||||
By default, the server will restart at the next good transition
|
||||
point (end of a series, etc) but passing immediate=True will restart
|
||||
it immediately.
|
||||
"""
|
||||
from bacommon.servermanager import ShutdownCommand, ShutdownReason
|
||||
self._enqueue_server_command(
|
||||
ShutdownCommand(reason=ShutdownReason.RESTARTING,
|
||||
immediate=immediate))
|
||||
|
||||
def _load_config(self) -> ServerConfig:
|
||||
user_config_path = 'config.yaml'
|
||||
|
||||
@ -113,85 +203,6 @@ class ServerManagerApp:
|
||||
# This is expected (readline doesn't exist under windows).
|
||||
pass
|
||||
|
||||
def run_interactive(self) -> None:
|
||||
"""Run the app loop to completion."""
|
||||
import code
|
||||
import signal
|
||||
|
||||
if self._running_interactive:
|
||||
raise RuntimeError('Already running interactively.')
|
||||
self._running_interactive = True
|
||||
|
||||
# Print basic usage info in interactive mode.
|
||||
if sys.stdin.isatty():
|
||||
print(f'{Clr.SBLU}BallisticaCore server manager starting up...\n'
|
||||
f'Use the "mgr" object to make live server adjustments.\n'
|
||||
f'Type "help(mgr)" for more information.{Clr.RST}')
|
||||
|
||||
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
||||
# need to register a SIGTERM handler so we have a chance to clean
|
||||
# up our child process when someone tells us to die. (and avoid
|
||||
# zombie processes)
|
||||
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()
|
||||
|
||||
# According to Python docs, default locals dict has __name__ set
|
||||
# to __console__ and __doc__ set to None; using that as start point.
|
||||
# https://docs.python.org/3/library/code.html
|
||||
locs = {'__name__': '__console__', '__doc__': None, 'mgr': self}
|
||||
|
||||
# Enable tab-completion if possible.
|
||||
self._enable_tab_completion(locs)
|
||||
|
||||
# Give ourself a lovely color prompt.
|
||||
sys.ps1 = f'{Clr.SGRN}>>> {Clr.RST}'
|
||||
sys.ps2 = f'{Clr.SGRN}... {Clr.RST}'
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
# TODO: make it possible to use IPython if the user has it available.
|
||||
try:
|
||||
code.interact(local=locs, banner='', exitmsg='')
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our BG thread still running.
|
||||
pass
|
||||
except BaseException as exc:
|
||||
print('Got unexpected exception: ', exc)
|
||||
|
||||
# Mark ourselves as shutting down and wait for the process to wrap up.
|
||||
self._done = True
|
||||
self._process_thread.join()
|
||||
|
||||
def cmd(self, statement: str) -> None:
|
||||
"""Exec a Python command on the current running server binary.
|
||||
|
||||
Note that commands are executed asynchronously and no status or
|
||||
return value is accessible from this manager app.
|
||||
"""
|
||||
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)
|
||||
|
||||
# Ideally we'd block here until the command was run so our prompt would
|
||||
# print after it's results. We currently don't get any response from
|
||||
# the app so the best we can do is block until our bg thread has sent
|
||||
# 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:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# One last short delay so if we come out *just* as the command is sent
|
||||
# we'll hopefully still give it enough time to process/print.
|
||||
time.sleep(0.1)
|
||||
|
||||
def _bg_thread_main(self) -> None:
|
||||
"""Top level method run by our bg thread."""
|
||||
while not self._done:
|
||||
@ -203,7 +214,7 @@ class ServerManagerApp:
|
||||
raise SystemExit()
|
||||
|
||||
def _run_server_cycle(self) -> None:
|
||||
"""Spin up the server process and run it until exit."""
|
||||
"""Spin up the server child-process and run it until exit."""
|
||||
|
||||
self._prep_process_environment()
|
||||
|
||||
@ -216,18 +227,13 @@ class ServerManagerApp:
|
||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||
|
||||
print(f'{Clr.SMAG}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')
|
||||
|
||||
# Set quit to True any time after launching the server
|
||||
# to gracefully quit it at the next clean opportunity
|
||||
# (the end of the current series, etc).
|
||||
self._config.quit = False
|
||||
self._config.quit_reason = None
|
||||
|
||||
# Do the thing.
|
||||
# No matter how this ends up, make sure the process is dead after.
|
||||
try:
|
||||
@ -250,16 +256,37 @@ class ServerManagerApp:
|
||||
with open('dist/ba_root/config.json', 'w') as outfile:
|
||||
outfile.write(json.dumps(bincfg))
|
||||
|
||||
def _enqueue_server_command(self, command: ServerCommand) -> None:
|
||||
"""Enqueue a command to be sent to the server.
|
||||
|
||||
Can be called from any thread.
|
||||
"""
|
||||
with self._process_commands_lock:
|
||||
self._process_commands.append(command)
|
||||
|
||||
def _send_server_command(self, command: ServerCommand) -> None:
|
||||
"""Send a command to the server.
|
||||
|
||||
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
|
||||
val = repr(pickle.dumps(command))
|
||||
assert '\n' not in val
|
||||
execcode = f'import ba._server; ba._server._cmd({val})\n'.encode()
|
||||
self._process.stdin.write(execcode)
|
||||
self._process.stdin.flush()
|
||||
|
||||
def _run_process_until_exit(self) -> None:
|
||||
assert self._process is not None
|
||||
assert self._process.stdin is not None
|
||||
|
||||
# Send the initial server config which should kick things off.
|
||||
# (but make sure its values are still valid first)
|
||||
dataclass_validate(self._config)
|
||||
cmd = make_server_command(ServerCommand.CONFIG, self._config)
|
||||
assert self._process.stdin is not None
|
||||
self._process.stdin.write(cmd)
|
||||
self._process.stdin.flush()
|
||||
self._send_server_command(StartServerModeCommand(self._config))
|
||||
|
||||
while True:
|
||||
|
||||
@ -270,40 +297,50 @@ class ServerManagerApp:
|
||||
# Pass along any commands to our process.
|
||||
with self._process_commands_lock:
|
||||
for incmd in self._process_commands:
|
||||
# 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.
|
||||
self._process.stdin.write((incmd + '\n').encode())
|
||||
self._process.stdin.flush()
|
||||
if isinstance(incmd, str):
|
||||
self._process.stdin.write((incmd + '\n').encode())
|
||||
self._process.stdin.flush()
|
||||
else:
|
||||
self._send_server_command(incmd)
|
||||
self._process_commands = []
|
||||
|
||||
# Request a restart after a while.
|
||||
# Request a soft restart after a while.
|
||||
assert self._process_launch_time is not None
|
||||
if (self._restart_minutes is not None
|
||||
and time.time() - self._process_launch_time >
|
||||
(self._restart_minutes * 60.0) and not self._config.quit):
|
||||
print('restart_minutes (' + str(self._restart_minutes) +
|
||||
'm) elapsed; will restart server process '
|
||||
'at next clean opportunity...')
|
||||
self._config.quit = True
|
||||
self._config.quit_reason = 'restarting'
|
||||
sincelaunch = time.time() - self._process_launch_time
|
||||
if (self._restart_minutes is not None and sincelaunch >
|
||||
(self._restart_minutes * 60.0)
|
||||
and not self._process_sent_auto_restart):
|
||||
print(f'{Clr.SMAG}restart_minutes ({self._restart_minutes})'
|
||||
f' elapsed; requesting child-process'
|
||||
f' soft restart...{Clr.RST}')
|
||||
self.restart()
|
||||
self._process_sent_auto_restart = True
|
||||
|
||||
# Watch for the process exiting.
|
||||
code: Optional[int] = self._process.poll()
|
||||
if code is not None:
|
||||
print(f'Server process exited with code {code}.')
|
||||
print(f'{Clr.SMAG}Server process exited'
|
||||
f' with code {code}.{Clr.RST}')
|
||||
time.sleep(1.0) # Keep things from moving too fast.
|
||||
self._process = self._process_launch_time = None
|
||||
self._reset_process_vars()
|
||||
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 _kill_process(self) -> None:
|
||||
"""End the server process if it still exists."""
|
||||
assert current_thread() is self._process_thread
|
||||
if self._process is None:
|
||||
return
|
||||
|
||||
print(f'{Clr.SBLU}Stopping server process...{Clr.RST}')
|
||||
print(f'{Clr.SMAG}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.
|
||||
@ -312,8 +349,8 @@ class ServerManagerApp:
|
||||
self._process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._process.kill()
|
||||
self._process = self._process_launch_time = None
|
||||
print(f'{Clr.SBLU}Server process stopped.{Clr.RST}')
|
||||
self._reset_process_vars()
|
||||
print(f'{Clr.SMAG}Server process stopped.{Clr.RST}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2020-05-02 for Ballistica version 1.5.0 build 20001</em></h4>
|
||||
<h4><em>last updated on 2020-05-03 for Ballistica version 1.5.0 build 20001</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>
|
||||
@ -3988,7 +3988,7 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
|
||||
</p>
|
||||
|
||||
<h3>Methods:</h3>
|
||||
<h5><a href="#method_ba_ServerController____init__"><constructor></a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__launch_server_session">launch_server_session()</a></h5>
|
||||
<h5><a href="#method_ba_ServerController____init__"><constructor></a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__shutdown">shutdown()</a></h5>
|
||||
<dl>
|
||||
<dt><h4><a name="method_ba_ServerController____init__"><constructor></a></dt></h4><dd>
|
||||
<p><span>ba.ServerController(config: ServerConfig)</span></p>
|
||||
@ -4005,10 +4005,10 @@ Should return True if action will be handled by us; False if the
|
||||
session should just continue on it's merry way.</p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__launch_server_session">launch_server_session()</a></dt></h4><dd>
|
||||
<p><span>launch_server_session(self) -> None</span></p>
|
||||
<dt><h4><a name="method_ba_ServerController__shutdown">shutdown()</a></dt></h4><dd>
|
||||
<p><span>shutdown(self, reason: ShutdownReason, immediate: bool) -> None</span></p>
|
||||
|
||||
<p>Kick off a host-session based on the current server config.</p>
|
||||
<p>Set the app to quit either now or at the next clean opportunity.</p>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@ -23,16 +23,15 @@ from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, Any, Tuple
|
||||
from typing_extensions import Literal
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerConfig:
|
||||
"""Configuration for the server manager script."""
|
||||
"""Configuration for the server manager app (ballisticacore_server)."""
|
||||
|
||||
# Name of our server in the public parties list.
|
||||
party_name: str = 'FFA'
|
||||
@ -109,52 +108,28 @@ class ServerConfig:
|
||||
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
|
||||
stats_url: Optional[str] = None
|
||||
|
||||
# FIXME REMOVE
|
||||
quit: bool = False
|
||||
|
||||
# FIXME REMOVE
|
||||
quit_reason: Optional[str] = None
|
||||
|
||||
|
||||
# NOTE: as much as possible, communication from the server-manager to the
|
||||
# child binary should go through this and not ad-hoc python string commands
|
||||
# child-process should go through these and not ad-hoc Python string commands
|
||||
# since this way is type safe.
|
||||
class ServerCommand(Enum):
|
||||
"""Command types that can be sent to the app in server-mode."""
|
||||
CONFIG = 'config'
|
||||
QUIT = 'quit'
|
||||
class ServerCommand:
|
||||
"""Base class for commands that can be sent to the server."""
|
||||
|
||||
|
||||
@overload
|
||||
def make_server_command(command: Literal[ServerCommand.CONFIG],
|
||||
payload: ServerConfig) -> bytes:
|
||||
"""Overload for CONFIG commands."""
|
||||
...
|
||||
@dataclass
|
||||
class StartServerModeCommand(ServerCommand):
|
||||
"""Tells the app to switch into 'server' mode."""
|
||||
config: ServerConfig
|
||||
|
||||
|
||||
@overload
|
||||
def make_server_command(command: Literal[ServerCommand.QUIT],
|
||||
payload: int) -> bytes:
|
||||
"""Overload for QUIT commands."""
|
||||
...
|
||||
class ShutdownReason(Enum):
|
||||
"""Reason a server is shutting down."""
|
||||
NONE = 'none'
|
||||
RESTARTING = 'restarting'
|
||||
|
||||
|
||||
def make_server_command(command: ServerCommand, payload: Any) -> bytes:
|
||||
"""Create a command that can be exec'ed on the server binary."""
|
||||
import pickle
|
||||
|
||||
# Pickle this stuff down to bytes and wrap it in a command to
|
||||
# extract/run it on the other end.
|
||||
val = repr(pickle.dumps((command, payload)))
|
||||
assert '\n' not in val
|
||||
return f'import ba._server; ba._server._cmd({val})\n'.encode()
|
||||
|
||||
|
||||
def extract_server_command(cmd: str) -> Tuple[ServerCommand, Any]:
|
||||
"""Given a server-command string, returns command objects."""
|
||||
|
||||
# Yes, eval is unsafe and all that, but this is only intended
|
||||
# for communication between a parent and child process so we
|
||||
# can live with it here.
|
||||
print('would extract', cmd)
|
||||
return ServerCommand.CONFIG, None
|
||||
@dataclass
|
||||
class ShutdownCommand(ServerCommand):
|
||||
"""Tells the server to shut down."""
|
||||
reason: ShutdownReason
|
||||
immediate: bool
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user