mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-29 18:53:22 +08:00
Added interactive server commands
This commit is contained in:
parent
8d5f506a62
commit
466f97e623
@ -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"
|
||||
}
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user