mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-04 22:43:17 +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/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/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/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/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/d1/bd/924adf7035ab6da8b2589f71b909",
|
"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/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/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",
|
"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/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/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/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/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/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/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/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/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",
|
"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>curhashes</w>
|
||||||
<w>curstate</w>
|
<w>curstate</w>
|
||||||
<w>curtime</w>
|
<w>curtime</w>
|
||||||
|
<w>curtimestr</w>
|
||||||
<w>customizebrowser</w>
|
<w>customizebrowser</w>
|
||||||
<w>cutscenes</w>
|
<w>cutscenes</w>
|
||||||
<w>cval</w>
|
<w>cval</w>
|
||||||
@ -565,6 +566,7 @@
|
|||||||
<w>excludepowerups</w>
|
<w>excludepowerups</w>
|
||||||
<w>excludetypes</w>
|
<w>excludetypes</w>
|
||||||
<w>excstr</w>
|
<w>excstr</w>
|
||||||
|
<w>execcode</w>
|
||||||
<w>execlocals</w>
|
<w>execlocals</w>
|
||||||
<w>executils</w>
|
<w>executils</w>
|
||||||
<w>exhash</w>
|
<w>exhash</w>
|
||||||
@ -1323,6 +1325,7 @@
|
|||||||
<w>playtypes</w>
|
<w>playtypes</w>
|
||||||
<w>plines</w>
|
<w>plines</w>
|
||||||
<w>plistlib</w>
|
<w>plistlib</w>
|
||||||
|
<w>plistname</w>
|
||||||
<w>plpt</w>
|
<w>plpt</w>
|
||||||
<w>plst</w>
|
<w>plst</w>
|
||||||
<w>plusbutton</w>
|
<w>plusbutton</w>
|
||||||
@ -1587,6 +1590,7 @@
|
|||||||
<w>serverdialog</w>
|
<w>serverdialog</w>
|
||||||
<w>serverget</w>
|
<w>serverget</w>
|
||||||
<w>servermanager</w>
|
<w>servermanager</w>
|
||||||
|
<w>servermode</w>
|
||||||
<w>serverput</w>
|
<w>serverput</w>
|
||||||
<w>serverutils</w>
|
<w>serverutils</w>
|
||||||
<w>sessionclass</w>
|
<w>sessionclass</w>
|
||||||
@ -1614,6 +1618,7 @@
|
|||||||
<w>shroom</w>
|
<w>shroom</w>
|
||||||
<w>shutil</w>
|
<w>shutil</w>
|
||||||
<w>simplesubclasses</w>
|
<w>simplesubclasses</w>
|
||||||
|
<w>sincelaunch</w>
|
||||||
<w>singledispatch</w>
|
<w>singledispatch</w>
|
||||||
<w>sirplus</w>
|
<w>sirplus</w>
|
||||||
<w>sitebuiltins</w>
|
<w>sitebuiltins</w>
|
||||||
@ -1827,6 +1832,7 @@
|
|||||||
<w>timeremaining</w>
|
<w>timeremaining</w>
|
||||||
<w>timestep</w>
|
<w>timestep</w>
|
||||||
<w>timestring</w>
|
<w>timestring</w>
|
||||||
|
<w>timestrval</w>
|
||||||
<w>timetype</w>
|
<w>timetype</w>
|
||||||
<w>tipstext</w>
|
<w>tipstext</w>
|
||||||
<w>titletext</w>
|
<w>titletext</w>
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
"ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc",
|
"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__/_powerup.cpython-37.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_profile.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__/_session.cpython-37.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_stats.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",
|
"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/_playlist.py",
|
||||||
"ba_data/python/ba/_powerup.py",
|
"ba_data/python/ba/_powerup.py",
|
||||||
"ba_data/python/ba/_profile.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/_session.py",
|
||||||
"ba_data/python/ba/_stats.py",
|
"ba_data/python/ba/_stats.py",
|
||||||
"ba_data/python/ba/_store.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/_coopgame.py \
|
||||||
build/ba_data/python/ba/_meta.py \
|
build/ba_data/python/ba/_meta.py \
|
||||||
build/ba_data/python/ba/_math.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/_appconfig.py \
|
||||||
build/ba_data/python/ba/_gameresults.py \
|
build/ba_data/python/ba/_gameresults.py \
|
||||||
build/ba_data/python/ba/_profile.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/_level.py \
|
||||||
build/ba_data/python/ba/_dependency.py \
|
build/ba_data/python/ba/_dependency.py \
|
||||||
build/ba_data/python/ba/_general.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/_account.py \
|
||||||
build/ba_data/python/ba/_music.py \
|
build/ba_data/python/ba/_music.py \
|
||||||
build/ba_data/python/ba/_lang.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__/_coopgame.cpython-37.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_meta.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__/_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__/_appconfig.cpython-37.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_gameresults.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 \
|
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__/_level.cpython-37.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_dependency.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__/_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__/_account.cpython-37.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_music.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 \
|
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: $^
|
@echo Compiling script: $^
|
||||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
@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/__pycache__/_appconfig.cpython-37.opt-1.pyc: \
|
||||||
build/ba_data/python/ba/_appconfig.py
|
build/ba_data/python/ba/_appconfig.py
|
||||||
@echo Compiling script: $^
|
@echo Compiling script: $^
|
||||||
@ -844,11 +849,6 @@ build/ba_data/python/ba/__pycache__/_general.cpython-37.opt-1.pyc: \
|
|||||||
@echo Compiling script: $^
|
@echo Compiling script: $^
|
||||||
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
|
@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/__pycache__/_account.cpython-37.opt-1.pyc: \
|
||||||
build/ba_data/python/ba/_account.py
|
build/ba_data/python/ba/_account.py
|
||||||
@echo Compiling script: $^
|
@echo Compiling script: $^
|
||||||
|
|||||||
@ -59,7 +59,7 @@ from ba._gameresults import TeamGameResults
|
|||||||
from ba._lang import Lstr, setlanguage, get_valid_languages
|
from ba._lang import Lstr, setlanguage, get_valid_languages
|
||||||
from ba._map import Map, getmaps
|
from ba._map import Map, getmaps
|
||||||
from ba._session import Session
|
from ba._session import Session
|
||||||
from ba._server import ServerController
|
from ba._servermode import ServerController
|
||||||
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
|
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
|
||||||
from ba._team import Team
|
from ba._team import Team
|
||||||
from ba._teamgame import TeamGameActivity
|
from ba._teamgame import TeamGameActivity
|
||||||
|
|||||||
@ -29,29 +29,35 @@ from efro.terminal import Clr
|
|||||||
from ba._enums import TimeType
|
from ba._enums import TimeType
|
||||||
from ba._freeforallsession import FreeForAllSession
|
from ba._freeforallsession import FreeForAllSession
|
||||||
from ba._dualteamsession import DualTeamSession
|
from ba._dualteamsession import DualTeamSession
|
||||||
from bacommon.servermanager import ServerConfig, ServerCommand
|
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
|
||||||
|
ShutdownCommand, ShutdownReason)
|
||||||
import _ba
|
import _ba
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Optional, Dict, Any, Type
|
from typing import Optional, Dict, Any, Type
|
||||||
import ba
|
import ba
|
||||||
|
from bacommon.servermanager import ServerConfig
|
||||||
|
|
||||||
|
|
||||||
def _cmd(command_data: bytes) -> None:
|
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
|
import pickle
|
||||||
command, payload = pickle.loads(command_data)
|
command = pickle.loads(command_data)
|
||||||
assert isinstance(command, ServerCommand)
|
assert isinstance(command, ServerCommand)
|
||||||
|
|
||||||
# We expect to receive a config command to kick things off.
|
if isinstance(command, StartServerModeCommand):
|
||||||
if command is ServerCommand.CONFIG:
|
|
||||||
assert isinstance(payload, ServerConfig)
|
|
||||||
assert _ba.app.server is None
|
assert _ba.app.server is None
|
||||||
_ba.app.server = ServerController(payload)
|
_ba.app.server = ServerController(command.config)
|
||||||
return
|
return
|
||||||
|
|
||||||
assert _ba.app.server is not None
|
if isinstance(command, ShutdownCommand):
|
||||||
print('WOULD DO OTHER SERVER COMMAND')
|
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:
|
class ServerController:
|
||||||
@ -68,6 +74,8 @@ class ServerController:
|
|||||||
self._run_server_wait_timer: Optional[ba.Timer] = None
|
self._run_server_wait_timer: Optional[ba.Timer] = None
|
||||||
self._next_stuck_login_warn_time = time.time() + 10.0
|
self._next_stuck_login_warn_time = time.time() + 10.0
|
||||||
self._first_run = True
|
self._first_run = True
|
||||||
|
self._shutdown_reason: Optional[ShutdownReason] = None
|
||||||
|
self._executing_shutdown = False
|
||||||
|
|
||||||
# Make note if they want us to import a playlist;
|
# Make note if they want us to import a playlist;
|
||||||
# we'll need to do that first if so.
|
# we'll need to do that first if so.
|
||||||
@ -76,59 +84,25 @@ class ServerController:
|
|||||||
self._playlist_fetch_got_response = False
|
self._playlist_fetch_got_response = False
|
||||||
self._playlist_fetch_code = -1
|
self._playlist_fetch_code = -1
|
||||||
|
|
||||||
self._config_server()
|
# Now sit around doing any pre-launch prep such as waiting for
|
||||||
|
# account sign-in or fetching playlists; this will kick off the
|
||||||
# Now sit around until we're signed in and then
|
# session once done.
|
||||||
# kick off the server.
|
|
||||||
with _ba.Context('ui'):
|
with _ba.Context('ui'):
|
||||||
self._run_server_wait_timer = _ba.Timer(
|
self._run_server_wait_timer = _ba.Timer(0.25,
|
||||||
0.25,
|
self._prepare_to_serve,
|
||||||
self._update_server_playlist_fetch,
|
timetype=TimeType.REAL,
|
||||||
timetype=TimeType.REAL,
|
repeat=True)
|
||||||
repeat=True)
|
|
||||||
|
|
||||||
def launch_server_session(self) -> None:
|
def shutdown(self, reason: ShutdownReason, immediate: bool) -> None:
|
||||||
"""Kick off a host-session based on the current server config."""
|
"""Set the app to quit either now or at the next clean opportunity."""
|
||||||
app = _ba.app
|
self._shutdown_reason = reason
|
||||||
appcfg = app.config
|
if immediate:
|
||||||
|
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
|
||||||
sessiontype = self._get_session_type()
|
self._execute_shutdown()
|
||||||
|
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
print(f'{Clr.SBLU}Shutdown initiated;'
|
||||||
|
f' server process will exit at the next clean opportunity.'
|
||||||
appcfg['Port'] = self._config.port
|
f'{Clr.RST}')
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def handle_transition(self) -> bool:
|
def handle_transition(self) -> bool:
|
||||||
"""Handle transitioning to a new ba.Session or quitting the app.
|
"""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
|
Should return True if action will be handled by us; False if the
|
||||||
session should just continue on it's merry way.
|
session should just continue on it's merry way.
|
||||||
"""
|
"""
|
||||||
print('FIXME: fill out server handle_transition()')
|
if self._shutdown_reason is not None:
|
||||||
# If the app is in server mode and this activity
|
self._execute_shutdown()
|
||||||
# if self._allow_server_transition and _ba.app.server_config_dirty:
|
return True
|
||||||
# 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
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _get_session_type(self) -> Type[ba.Session]:
|
def _execute_shutdown(self) -> None:
|
||||||
|
from ba._lang import Lstr
|
||||||
# Convert string session type to the class.
|
if self._executing_shutdown:
|
||||||
# Hmm should we just keep this as a string?
|
return
|
||||||
if self._config.session_type == 'ffa':
|
self._executing_shutdown = True
|
||||||
return FreeForAllSession
|
timestrval = time.strftime('%c')
|
||||||
if self._config.session_type == 'teams':
|
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||||
return DualTeamSession
|
# FIXME: Should add a server-screen-message call.
|
||||||
raise RuntimeError(
|
# (so we could send this an an Lstr)
|
||||||
f'Invalid session_type: "{self._config.session_type}"')
|
_ba.chat_message(
|
||||||
|
Lstr(resource='internal.serverRestartingText').evaluate())
|
||||||
def _update_server_playlist_fetch(self) -> None:
|
print(f'{Clr.SBLU}Exiting for server-restart'
|
||||||
|
f' at {timestrval}{Clr.RST}')
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
can_launch = False
|
# FIXME: Should add a server-screen-message call.
|
||||||
|
# (so we could send this an an Lstr)
|
||||||
# If we're trying to fetch a playlist, we do that first.
|
print(f'{Clr.SBLU}Exiting for server-shutdown'
|
||||||
if self._playlist_fetch_running:
|
f' at {timestrval}{Clr.RST}')
|
||||||
|
_ba.chat_message(
|
||||||
# Send request if we haven't.
|
Lstr(resource='internal.serverShuttingDownText').evaluate())
|
||||||
if not self._playlist_fetch_sent_request:
|
with _ba.Context('ui'):
|
||||||
|
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||||
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'])
|
|
||||||
|
|
||||||
def _run_access_check(self) -> None:
|
def _run_access_check(self) -> None:
|
||||||
"""Check with the master server to see if we're likely joinable."""
|
"""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?)')
|
print('error on UDP port access check (internet down?)')
|
||||||
else:
|
else:
|
||||||
if data['accessible']:
|
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' Your server appears to be joinable from the'
|
||||||
f' internet.{Clr.RST}')
|
f' internet.{Clr.RST}')
|
||||||
else:
|
else:
|
||||||
@ -280,18 +166,123 @@ class ServerController:
|
|||||||
f' Your server does not appear to be joinable'
|
f' Your server does not appear to be joinable'
|
||||||
f' from the internet.{Clr.RST}')
|
f' from the internet.{Clr.RST}')
|
||||||
|
|
||||||
def _config_server(self) -> None:
|
def _prepare_to_serve(self) -> None:
|
||||||
"""Apply server config changes that can take effect immediately.
|
|
||||||
|
|
||||||
(party name, etc)
|
signed_in = _ba.get_account_state() == 'signed_in'
|
||||||
"""
|
|
||||||
|
|
||||||
_ba.app.config['Auto Balance Teams'] = (
|
if not signed_in:
|
||||||
self._config.auto_balance_teams)
|
|
||||||
|
|
||||||
|
# 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_max_size(self._config.max_party_size)
|
||||||
_ba.set_public_party_name(self._config.party_name)
|
_ba.set_public_party_name(self._config.party_name)
|
||||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||||
|
|
||||||
# Call set-enabled last (will push state to the cloud).
|
# Call set-enabled last (will push state to the cloud).
|
||||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
_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.terminal import Clr
|
||||||
from efro.dataclassutils import dataclass_assign, dataclass_validate
|
from efro.dataclassutils import dataclass_assign, dataclass_validate
|
||||||
from bacommon.servermanager import (ServerConfig, ServerCommand,
|
from bacommon.servermanager import (ServerConfig, StartServerModeCommand)
|
||||||
make_server_command)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict, Union
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
|
from bacommon.servermanager import ServerCommand
|
||||||
|
|
||||||
|
|
||||||
class ServerManagerApp:
|
class ServerManagerApp:
|
||||||
@ -58,12 +58,13 @@ class ServerManagerApp:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._config = self._load_config()
|
self._config = self._load_config()
|
||||||
self._done = False
|
self._done = False
|
||||||
self._process_commands: List[str] = []
|
self._process_commands: List[Union[str, ServerCommand]] = []
|
||||||
self._process_commands_lock = Lock()
|
self._process_commands_lock = Lock()
|
||||||
self._restart_minutes: Optional[float] = 360.0
|
self._restart_minutes: Optional[float] = 360.0
|
||||||
self._running_interactive = False
|
self._running_interactive = False
|
||||||
self._process: Optional[subprocess.Popen[bytes]] = None
|
self._process: Optional[subprocess.Popen[bytes]] = None
|
||||||
self._process_launch_time: Optional[float] = None
|
self._process_launch_time: Optional[float] = None
|
||||||
|
self._process_sent_auto_restart = False
|
||||||
self._process_thread: Optional[Thread] = None
|
self._process_thread: Optional[Thread] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -85,6 +86,95 @@ class ServerManagerApp:
|
|||||||
"""
|
"""
|
||||||
return self._restart_minutes
|
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:
|
def _load_config(self) -> ServerConfig:
|
||||||
user_config_path = 'config.yaml'
|
user_config_path = 'config.yaml'
|
||||||
|
|
||||||
@ -113,85 +203,6 @@ class ServerManagerApp:
|
|||||||
# This is expected (readline doesn't exist under windows).
|
# This is expected (readline doesn't exist under windows).
|
||||||
pass
|
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:
|
def _bg_thread_main(self) -> None:
|
||||||
"""Top level method run by our bg thread."""
|
"""Top level method run by our bg thread."""
|
||||||
while not self._done:
|
while not self._done:
|
||||||
@ -203,7 +214,7 @@ class ServerManagerApp:
|
|||||||
raise SystemExit()
|
raise SystemExit()
|
||||||
|
|
||||||
def _run_server_cycle(self) -> None:
|
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()
|
self._prep_process_environment()
|
||||||
|
|
||||||
@ -216,18 +227,13 @@ class ServerManagerApp:
|
|||||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
# slight behavior tweaks. Hmm; should this be an argument instead?
|
||||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||||
|
|
||||||
|
print(f'{Clr.SMAG}Launching server child-process...{Clr.RST}')
|
||||||
binary_name = ('ballisticacore_headless.exe'
|
binary_name = ('ballisticacore_headless.exe'
|
||||||
if os.name == 'nt' else './ballisticacore_headless')
|
if os.name == 'nt' else './ballisticacore_headless')
|
||||||
self._process = subprocess.Popen([binary_name, '-cfgdir', 'ba_root'],
|
self._process = subprocess.Popen([binary_name, '-cfgdir', 'ba_root'],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
cwd='dist')
|
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.
|
# Do the thing.
|
||||||
# No matter how this ends up, make sure the process is dead after.
|
# No matter how this ends up, make sure the process is dead after.
|
||||||
try:
|
try:
|
||||||
@ -250,16 +256,37 @@ class ServerManagerApp:
|
|||||||
with open('dist/ba_root/config.json', 'w') as outfile:
|
with open('dist/ba_root/config.json', 'w') as outfile:
|
||||||
outfile.write(json.dumps(bincfg))
|
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:
|
def _run_process_until_exit(self) -> None:
|
||||||
assert self._process is not None
|
assert self._process is not None
|
||||||
|
assert self._process.stdin is not None
|
||||||
|
|
||||||
# Send the initial server config which should kick things off.
|
# Send the initial server config which should kick things off.
|
||||||
# (but make sure its values are still valid first)
|
# (but make sure its values are still valid first)
|
||||||
dataclass_validate(self._config)
|
dataclass_validate(self._config)
|
||||||
cmd = make_server_command(ServerCommand.CONFIG, self._config)
|
self._send_server_command(StartServerModeCommand(self._config))
|
||||||
assert self._process.stdin is not None
|
|
||||||
self._process.stdin.write(cmd)
|
|
||||||
self._process.stdin.flush()
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
@ -270,40 +297,50 @@ class ServerManagerApp:
|
|||||||
# Pass along any commands to our process.
|
# Pass along any commands to our process.
|
||||||
with self._process_commands_lock:
|
with self._process_commands_lock:
|
||||||
for incmd in self._process_commands:
|
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.
|
# in any proper structure.
|
||||||
self._process.stdin.write((incmd + '\n').encode())
|
if isinstance(incmd, str):
|
||||||
self._process.stdin.flush()
|
self._process.stdin.write((incmd + '\n').encode())
|
||||||
|
self._process.stdin.flush()
|
||||||
|
else:
|
||||||
|
self._send_server_command(incmd)
|
||||||
self._process_commands = []
|
self._process_commands = []
|
||||||
|
|
||||||
# Request a restart after a while.
|
# Request a soft restart after a while.
|
||||||
assert self._process_launch_time is not None
|
assert self._process_launch_time is not None
|
||||||
if (self._restart_minutes is not None
|
sincelaunch = time.time() - self._process_launch_time
|
||||||
and time.time() - self._process_launch_time >
|
if (self._restart_minutes is not None and sincelaunch >
|
||||||
(self._restart_minutes * 60.0) and not self._config.quit):
|
(self._restart_minutes * 60.0)
|
||||||
print('restart_minutes (' + str(self._restart_minutes) +
|
and not self._process_sent_auto_restart):
|
||||||
'm) elapsed; will restart server process '
|
print(f'{Clr.SMAG}restart_minutes ({self._restart_minutes})'
|
||||||
'at next clean opportunity...')
|
f' elapsed; requesting child-process'
|
||||||
self._config.quit = True
|
f' soft restart...{Clr.RST}')
|
||||||
self._config.quit_reason = 'restarting'
|
self.restart()
|
||||||
|
self._process_sent_auto_restart = True
|
||||||
|
|
||||||
# Watch for the process exiting.
|
# Watch for the process exiting.
|
||||||
code: Optional[int] = self._process.poll()
|
code: Optional[int] = self._process.poll()
|
||||||
if code is not None:
|
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.
|
time.sleep(1.0) # Keep things from moving too fast.
|
||||||
self._process = self._process_launch_time = None
|
self._reset_process_vars()
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(0.25)
|
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:
|
def _kill_process(self) -> None:
|
||||||
"""End the server process if it still exists."""
|
"""End the server process if it still exists."""
|
||||||
assert current_thread() is self._process_thread
|
assert current_thread() is self._process_thread
|
||||||
if self._process is None:
|
if self._process is None:
|
||||||
return
|
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.
|
# First, ask it nicely to die and give it a moment.
|
||||||
# If that doesn't work, bring down the hammer.
|
# If that doesn't work, bring down the hammer.
|
||||||
@ -312,8 +349,8 @@ class ServerManagerApp:
|
|||||||
self._process.wait(timeout=10)
|
self._process.wait(timeout=10)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self._process.kill()
|
self._process.kill()
|
||||||
self._process = self._process_launch_time = None
|
self._reset_process_vars()
|
||||||
print(f'{Clr.SBLU}Server process stopped.{Clr.RST}')
|
print(f'{Clr.SMAG}Server process stopped.{Clr.RST}')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||||
<h4><em>last updated on 2020-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,
|
<p>This page documents the Python classes and functions in the 'ba' module,
|
||||||
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||||
<hr>
|
<hr>
|
||||||
@ -3988,7 +3988,7 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Methods:</h3>
|
<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>
|
<dl>
|
||||||
<dt><h4><a name="method_ba_ServerController____init__"><constructor></a></dt></h4><dd>
|
<dt><h4><a name="method_ba_ServerController____init__"><constructor></a></dt></h4><dd>
|
||||||
<p><span>ba.ServerController(config: ServerConfig)</span></p>
|
<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>
|
session should just continue on it's merry way.</p>
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
<dt><h4><a name="method_ba_ServerController__launch_server_session">launch_server_session()</a></dt></h4><dd>
|
<dt><h4><a name="method_ba_ServerController__shutdown">shutdown()</a></dt></h4><dd>
|
||||||
<p><span>launch_server_session(self) -> None</span></p>
|
<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>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@ -23,16 +23,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, overload
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Optional, Any, Tuple
|
from typing import Optional
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ServerConfig:
|
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.
|
# Name of our server in the public parties list.
|
||||||
party_name: str = 'FFA'
|
party_name: str = 'FFA'
|
||||||
@ -109,52 +108,28 @@ class ServerConfig:
|
|||||||
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
|
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
|
||||||
stats_url: Optional[str] = None
|
stats_url: Optional[str] = None
|
||||||
|
|
||||||
# FIXME REMOVE
|
|
||||||
quit: bool = False
|
|
||||||
|
|
||||||
# FIXME REMOVE
|
|
||||||
quit_reason: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: as much as possible, communication from the server-manager to the
|
# NOTE: as much as possible, communication from the server-manager to the
|
||||||
# child 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.
|
# since this way is type safe.
|
||||||
class ServerCommand(Enum):
|
class ServerCommand:
|
||||||
"""Command types that can be sent to the app in server-mode."""
|
"""Base class for commands that can be sent to the server."""
|
||||||
CONFIG = 'config'
|
|
||||||
QUIT = 'quit'
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@dataclass
|
||||||
def make_server_command(command: Literal[ServerCommand.CONFIG],
|
class StartServerModeCommand(ServerCommand):
|
||||||
payload: ServerConfig) -> bytes:
|
"""Tells the app to switch into 'server' mode."""
|
||||||
"""Overload for CONFIG commands."""
|
config: ServerConfig
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
class ShutdownReason(Enum):
|
||||||
def make_server_command(command: Literal[ServerCommand.QUIT],
|
"""Reason a server is shutting down."""
|
||||||
payload: int) -> bytes:
|
NONE = 'none'
|
||||||
"""Overload for QUIT commands."""
|
RESTARTING = 'restarting'
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def make_server_command(command: ServerCommand, payload: Any) -> bytes:
|
@dataclass
|
||||||
"""Create a command that can be exec'ed on the server binary."""
|
class ShutdownCommand(ServerCommand):
|
||||||
import pickle
|
"""Tells the server to shut down."""
|
||||||
|
reason: ShutdownReason
|
||||||
# Pickle this stuff down to bytes and wrap it in a command to
|
immediate: bool
|
||||||
# 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
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user