mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-03-22 01:26:31 +08:00
Refactored server wrapper to use interactive python interpreter
This commit is contained in:
parent
ea7becef02
commit
3e3b1b0fc0
3
.idea/dictionaries/ericf.xml
generated
3
.idea/dictionaries/ericf.xml
generated
@ -86,6 +86,7 @@
|
||||
<w>armeabi</w>
|
||||
<w>arraymodule</w>
|
||||
<w>asdict</w>
|
||||
<w>aspx</w>
|
||||
<w>assertnode</w>
|
||||
<w>assetbundle</w>
|
||||
<w>assetcache</w>
|
||||
@ -161,6 +162,7 @@
|
||||
<w>bgmodel</w>
|
||||
<w>bgterrain</w>
|
||||
<w>bgtex</w>
|
||||
<w>bgthread</w>
|
||||
<w>bhval</w>
|
||||
<w>binc</w>
|
||||
<w>bindcode</w>
|
||||
@ -1328,6 +1330,7 @@
|
||||
<w>pragmas</w>
|
||||
<w>prch</w>
|
||||
<w>prec</w>
|
||||
<w>preexec</w>
|
||||
<w>preflightfast</w>
|
||||
<w>preflightfull</w>
|
||||
<w>preflighting</w>
|
||||
|
||||
@ -60,7 +60,6 @@ class ServerController:
|
||||
"""
|
||||
|
||||
def __init__(self, config: ServerConfig) -> None:
|
||||
print('Server()')
|
||||
|
||||
self._config = config
|
||||
self._playlist_name = '__default__'
|
||||
@ -102,7 +101,7 @@ class ServerController:
|
||||
'with a signed in server account')
|
||||
|
||||
if self._first_run:
|
||||
print((('BallisticaCore headless '
|
||||
print((('BallisticaCore '
|
||||
if app.headless_build else 'BallisticaCore ') +
|
||||
str(app.version) + ' (' + str(app.build_number) +
|
||||
') entering server-mode ' + time.strftime('%c')))
|
||||
|
||||
@ -43,6 +43,7 @@ from bacommon.serverutils import (ServerConfig, ServerCommand,
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, List
|
||||
from types import FrameType
|
||||
|
||||
|
||||
class App:
|
||||
@ -62,31 +63,22 @@ class App:
|
||||
self._binary_path = self._get_binary_path()
|
||||
self._config = ServerConfig()
|
||||
|
||||
# Launch a thread to listen for input
|
||||
# (in daemon mode so it won't prevent us from dying)
|
||||
self._input_commands: List[str] = []
|
||||
thread = threading.Thread(target=self._read_input)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
# Print basic usage info in interactive mode.
|
||||
if sys.stdin.isatty():
|
||||
print('BallisticaCore Server wrapper starting up...')
|
||||
print('BallisticaCore server manager starting up...')
|
||||
|
||||
self._input_commands: List[str] = []
|
||||
|
||||
# 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._done = False
|
||||
self._process: Optional[subprocess.Popen[bytes]] = None
|
||||
self._process_launch_time: Optional[float] = None
|
||||
|
||||
# The standard python exit/quit help messages don't apply here
|
||||
# so let's get rid of them.
|
||||
del __builtins__.exit
|
||||
del __builtins__.quit
|
||||
|
||||
def _get_binary_path(self) -> str:
|
||||
"""Locate the game binary we'll run."""
|
||||
"""Locate the game binary that we'll use."""
|
||||
if os.name == 'nt':
|
||||
test_paths = ['ballisticacore_headless.exe']
|
||||
else:
|
||||
@ -100,16 +92,47 @@ class App:
|
||||
"""Read from stdin and queue results for the app to handle."""
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
print('read line', line)
|
||||
self._input_commands.append(line.strip())
|
||||
|
||||
def run(self) -> None:
|
||||
def run_interactive(self) -> None:
|
||||
"""Run the app loop to completion."""
|
||||
import code
|
||||
import signal
|
||||
|
||||
# We currently never stop until explicitly killed.
|
||||
while True:
|
||||
# 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
|
||||
# zombie processes)
|
||||
signal.signal(signal.SIGTERM, self._handle_term_signal)
|
||||
|
||||
# Fire off a background thread to wrangle our server binaries.
|
||||
bgthread = threading.Thread(target=self._bg_thread_main)
|
||||
bgthread.start()
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
try:
|
||||
code.interact(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 bgthread to wrap up.
|
||||
self._done = True
|
||||
bgthread.join()
|
||||
|
||||
def _bg_thread_main(self) -> None:
|
||||
while not self._done:
|
||||
self._run_server_cycle()
|
||||
|
||||
def _handle_term_signal(self, sig: int, frame: FrameType) -> None:
|
||||
"""Handle signals (will always run in the main thread)."""
|
||||
del sig, frame # Unused.
|
||||
raise SystemExit()
|
||||
|
||||
def _run_server_cycle(self) -> None:
|
||||
"""Bring up the server binary and run it until exit."""
|
||||
|
||||
@ -118,8 +141,27 @@ class App:
|
||||
# Launch the binary and grab its stdin;
|
||||
# we'll use this to feed it commands.
|
||||
self._process_launch_time = time.time()
|
||||
self._process = subprocess.Popen(
|
||||
[self._binary_path, '-cfgdir', 'ba_root'], stdin=subprocess.PIPE)
|
||||
|
||||
# We don't want our subprocess to respond to Ctrl-C; we want to handle
|
||||
# that ourself. So we need to do a bit of magic to accomplish that.
|
||||
args = [self._binary_path, '-cfgdir', 'ba_root']
|
||||
if sys.platform.startswith('win'):
|
||||
# https://msdn.microsoft.com/en-us/library/windows/
|
||||
# desktop/ms684863(v=vs.85).aspx
|
||||
# CREATE_NEW_PROCESS_GROUP=0x00000200 -> If this flag is
|
||||
# specified, CTRL+C signals will be disabled
|
||||
self._process = subprocess.Popen(args,
|
||||
stdin=subprocess.PIPE,
|
||||
creationflags=0x00000200)
|
||||
else:
|
||||
# Note: Python docs tell us preexec_fn is unsafe with threads.
|
||||
# https://docs.python.org/3/library/subprocess.html
|
||||
# Perhaps we should just give the ballistica binary itself an
|
||||
# option to ignore interrupt signals.
|
||||
self._process = subprocess.Popen( # pylint: disable=W1509
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
preexec_fn=self._subprocess_pre_exec)
|
||||
|
||||
# Set quit to True any time after launching the server
|
||||
# to gracefully quit it at the next clean opportunity
|
||||
@ -136,6 +178,11 @@ class App:
|
||||
self._kill_process()
|
||||
raise
|
||||
|
||||
def _subprocess_pre_exec(self) -> None:
|
||||
"""To ignore CTRL+C signal in the new process."""
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
def _setup_process_config(self) -> None:
|
||||
"""Write files that must exist at process launch."""
|
||||
os.makedirs('ba_root', exist_ok=True)
|
||||
@ -163,6 +210,11 @@ class App:
|
||||
# Now just sleep and run commands until the process exits.
|
||||
while True:
|
||||
|
||||
# If the app is trying to shut down, crap out of the subprocess.
|
||||
if self._done:
|
||||
self._kill_process()
|
||||
return
|
||||
|
||||
# Pass along any commands that have come in through stdin.
|
||||
for incmd in self._input_commands:
|
||||
print('WOULD PASS ALONG COMMAND', incmd)
|
||||
@ -181,7 +233,8 @@ class App:
|
||||
# Watch for the process exiting.
|
||||
code: Optional[int] = self._process.poll()
|
||||
if code is not None:
|
||||
print('Server process exited with code ' + str(code))
|
||||
print(f'Server process exited with code {code}.')
|
||||
time.sleep(1.0) # Keep things from moving too fast.
|
||||
self._process = None
|
||||
self._process_launch_time = None
|
||||
break
|
||||
@ -204,7 +257,4 @@ class App:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
App().run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
App().run_interactive()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user