Added interactive server commands

This commit is contained in:
Eric Froemling 2020-04-28 01:04:24 -07:00
parent 8d5f506a62
commit 466f97e623
2 changed files with 69 additions and 31 deletions

View File

@ -4136,12 +4136,12 @@
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/24/01/cdf0206ccaaa4999d887631683d9",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/20/ec/0cf2db1fccd23db8813353ead7b5",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/71/f3/fe9bc55d33255ad3197795da820f",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3a/8c/c30406a904ee2c3692e4f3faf057",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/48/3c/8eb7975b35d68e2303d3fee4e949",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2b/05/11fc566e712691f90a7d3db207f2",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f7/2a/41de3d8aab113a9af14743f5ddeb",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/aa/1a/642123b5642b33b91570c3dd0c1d",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/49/53/28acc8c627c2c22e8f7a20e38321",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b8/30/0264c9d438dd2c9044eef555ee2d",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d5/b4/ae285fff27d49facf3c0ad7410ed",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/5b/83/495e09df6e194d56e844bd855101",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9f/bc/126139d9b5bc33e0501c1182ba1f"
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1d/cd/35b9a612245a642f36f5ec1b0966",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1e/98/d1156cd3718aa52c59ed972170ca",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e1/73/cf488e42313eb5cef34d9f2eacd5",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d2/65/7f2e288a5cfb7ffe566691175784"
}

View File

@ -19,7 +19,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality for running a BallisticaCore server."""
"""BallisticaCore server management."""
from __future__ import annotations
import sys
@ -46,11 +46,11 @@ if TYPE_CHECKING:
from types import FrameType
class App:
"""Runs a BallisticaCore server.
class ServerManagerApp:
"""An app which manages BallisticaCore server execution.
Handles passing config values to the game and periodically restarting
the game binary to keep things fresh.
Handles configuring, launching, re-launching, and controlling
BallisticaCore binaries operating in server mode.
"""
def __init__(self) -> None:
@ -63,16 +63,14 @@ class App:
self._binary_path = self._get_binary_path()
self._config = ServerConfig()
# Print basic usage info in interactive mode.
if sys.stdin.isatty():
print('BallisticaCore server manager starting up...')
self._input_commands: List[str] = []
self._binary_commands: List[str] = []
self._binary_commands_lock = threading.Lock()
# The server-binary will get relaunched after this amount of time
# (combats memory leaks or other cruft that has built up).
self._restart_minutes = 360.0
self._running_interactive = False
self._done = False
self._process: Optional[subprocess.Popen[bytes]] = None
self._process_launch_time: Optional[float] = None
@ -88,17 +86,21 @@ class App:
return path
raise RuntimeError('Unable to locate ballisticacore_headless binary.')
def _read_input(self) -> None:
"""Read from stdin and queue results for the app to handle."""
while True:
line = sys.stdin.readline()
self._input_commands.append(line.strip())
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('BallisticaCore server manager starting up...\n'
'Use the "mgr" object to make live server adjustments.\n'
'Type "help(mgr)" for more information.')
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
# need to register a SIGTERM handler if we want a chance to clean
# up our child process when someone tells us to die. (and avoid
@ -109,9 +111,14 @@ class App:
bgthread = threading.Thread(target=self._bg_thread_main)
bgthread.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}
# Now just sit in an interpreter.
try:
code.interact(banner='', exitmsg='')
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
@ -124,6 +131,33 @@ class App:
self._done = True
bgthread.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._binary_commands_lock:
self._binary_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._binary_commands_lock:
if not self._binary_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:
while not self._done:
self._run_server_cycle()
@ -136,7 +170,7 @@ class App:
def _run_server_cycle(self) -> None:
"""Bring up the server binary and run it until exit."""
self._setup_process_config()
self._prep_process_environment()
# Launch the binary and grab its stdin;
# we'll use this to feed it commands.
@ -181,7 +215,7 @@ class App:
import signal
signal.signal(signal.SIGINT, signal.SIG_IGN)
def _setup_process_config(self) -> None:
def _prep_process_environment(self) -> None:
"""Write files that must exist at process launch."""
os.makedirs('ba_root', exist_ok=True)
if os.path.exists('ba_root/config.json'):
@ -213,9 +247,13 @@ class App:
# Pass along any commands to the subprocess..
# FIXME add a lock for this...
for incmd in self._input_commands:
print('WOULD PASS ALONG COMMAND', incmd)
self._input_commands = []
with self._binary_commands_lock:
for incmd in self._binary_commands:
# 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()
self._binary_commands = []
# Request a restart after a while.
assert self._process_launch_time is not None
@ -242,7 +280,7 @@ class App:
if self._process is None:
return
print('Stopping server process...')
print('Stopping server subprocess...')
# First, ask it nicely to die and give it a moment.
# If that doesn't work, bring down the hammer.
@ -252,8 +290,8 @@ class App:
except subprocess.TimeoutExpired:
self._process.kill()
self._process = self._process_launch_time = None
print('Server process stopped.')
print('Server subprocess stopped.')
if __name__ == '__main__':
App().run_interactive()
ServerManagerApp().run_interactive()