Added 'clientlist' and 'kick' server commands

This commit is contained in:
Eric Froemling 2020-05-03 18:23:38 -07:00
parent c4f06f5f96
commit acaa9a90eb
7 changed files with 174 additions and 40 deletions

View File

@ -4132,16 +4132,16 @@
"assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c",
"assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb",
"assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe",
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/a7/f1a153619272b4882dc0d1fcf228",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bd/08/3a948bec6a0553109361ab201b7f",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/69/4e/34a3913539ae323f32c6029aee61",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/2c/3b0f126c249753469779ac7abd99",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/f5/252523aff43024c6ced461abcc04",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/f5/35f0e8d8259c37389fe2d60becbc",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ca/ad/c4e22ab2e7de1356f47aa4f8dc07",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/02/dc/bea2f3a3a862b9e587043f6bd73f",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d7/df/80ceb60dc041e2b9a69f0a3da9ea",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b1/86/dddc66545c917e3a028fc6a6caea",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a2/69/b183a77cd9e8a91ef550c4e7bcec",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a0/17/d7aca9e4c6e604993328cd389356"
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a2/5b/8598da1884bbe060f495d0273d7d",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/95/5e30fd47e6befa8f9a370a52be21",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/60/3a/1edfbfc7adcbda3f6734f9282756",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/34/a1/f7d0bf6b709a757418825a1cc24f",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/e9/8a29cf17f4434f783fab73afc349",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/71/9d/02c9e2ba7c91941991552228c66c",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9b/35/42e8f879ac5b07ba2d8caee897dc",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3f/3e/20eb6ac1043ac0417c2b7728f7bd",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b9/75/6b1523ec9b0d510870b4695a6613",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7d/04/b882bbfd0197248bca49016356c1",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/13/50/1fb612a251431229703317bd81e5",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ec/3c/ce84db1040251fb5a11b0c7dfd7b"
}

View File

@ -296,6 +296,7 @@
<w>cleancheck</w>
<w>cleanlist</w>
<w>clientid</w>
<w>clientlist</w>
<w>clionbin</w>
<w>clioncode</w>
<w>clionroot</w>
@ -1956,6 +1957,7 @@
<w>vartype</w>
<w>vcxproj</w>
<w>venv</w>
<w>versioning</w>
<w>versionpredicate</w>
<w>vert</w>
<w>verts</w>

View File

@ -30,7 +30,9 @@ from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
ShutdownCommand, ShutdownReason)
ShutdownCommand, ShutdownReason,
BroadcastCommand, ClientListCommand,
KickCommand)
import _ba
if TYPE_CHECKING:
@ -56,6 +58,22 @@ def _cmd(command_data: bytes) -> None:
immediate=command.immediate)
return
if isinstance(command, BroadcastCommand):
assert _ba.app.server is not None
_ba.app.server.broadcast_message(command.message)
return
if isinstance(command, ClientListCommand):
assert _ba.app.server is not None
_ba.app.server.print_client_list()
return
if isinstance(command, KickCommand):
assert _ba.app.server is not None
_ba.app.server.kick(client_id=command.client_id,
ban_time=command.ban_time)
return
print(f'{Clr.SRED}ERROR: server process'
f' got unknown command: {type(command)}{Clr.RST}')
@ -93,6 +111,48 @@ class ServerController:
timetype=TimeType.REAL,
repeat=True)
def broadcast_message(self, message: str) -> None:
"""Broadcast a message to all connected clients."""
# FIXME: Should add a proper call for this, which would allow
# us to use Lstr values and colors and whatnot.
_ba.chat_message(message)
def print_client_list(self) -> None:
"""Print info about all connected clients."""
import json
roster = _ba.get_game_roster()
title1 = 'Client ID'
title2 = 'Account Name'
title3 = 'Players'
col1 = 10
col2 = 16
out = (f'{Clr.BLD}'
f'{title1:<{col1}} {title2:<{col2}} {title3}'
f'{Clr.RST}')
for client in roster:
if client['client_id'] == -1:
continue
spec = json.loads(client['specString'])
name = spec['n']
players = ', '.join(n['name'] for n in client['players'])
clientid = client['client_id']
out += f'\n{clientid:<{col1}} {name:<{col2}} {players}'
print(out)
def kick(self, client_id: int, ban_time: Optional[int]) -> None:
"""Kick the provided client id.
ban_time is provided in seconds.
If ban_time is None, ban duration will be determined automatically.
Pass 0 or a negative number for no ban time.
"""
# FIXME: this case should be handled under the hood.
if ban_time is None:
ban_time = 300
_ba.disconnect_client(client_id=client_id, ban_time=ban_time)
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
@ -124,19 +184,15 @@ class ServerController:
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(
self.broadcast_message(
Lstr(resource='internal.serverRestartingText').evaluate())
print(f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}{Clr.RST}')
else:
# FIXME: Should add a server-screen-message call.
# (so we could send this an an Lstr)
self.broadcast_message(
Lstr(resource='internal.serverShuttingDownText').evaluate())
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)
@ -153,18 +209,22 @@ class ServerController:
)
def _access_check_response(self, data: Optional[Dict[str, Any]]) -> None:
gameport = _ba.get_game_port()
# gameport = _ba.get_game_port()
if data is None:
print('error on UDP port access check (internet down?)')
else:
addr = data['address']
port = data['port']
if data['accessible']:
print(f'{Clr.SBLU}UDP port {gameport} access check successful.'
f' Your server appears to be joinable from the'
f' internet.{Clr.RST}')
print(f'{Clr.SBLU}Master server access check of {addr}'
f' udp port {port} succeeded.\n'
f'Your server appears to be'
f' joinable from the internet.{Clr.RST}')
else:
print(f'{Clr.SRED}UDP port {gameport} access check failed.'
f' Your server does not appear to be joinable'
f' from the internet.{Clr.RST}')
print(f'{Clr.SRED}Master server access check of {addr}'
f' udp port {port} failed.\n'
f'Your server does not appear to be'
f' joinable from the internet.{Clr.RST}')
def _prepare_to_serve(self) -> None:
signed_in = _ba.get_account_state() == 'signed_in'

View File

@ -48,6 +48,10 @@ if TYPE_CHECKING:
from types import FrameType
from bacommon.servermanager import ServerCommand
# Not sure how much versioning we'll do with this, but this will get
# printed at startup in case we need it.
VERSION_STR = '1.0'
class ServerManagerApp:
"""An app which manages BallisticaCore server execution.
@ -101,7 +105,8 @@ class ServerManagerApp:
# Print basic usage info in interactive mode.
if sys.stdin.isatty():
print(f'{Clr.SMAG}BallisticaCore server manager starting up...\n'
print(f'{Clr.SMAG}BallisticaCore server manager {VERSION_STR}'
f' starting up...\n'
f'Use the "mgr" object to make live server adjustments.\n'
f'Type "help(mgr)" for more information.{Clr.RST}')
@ -115,22 +120,19 @@ class ServerManagerApp:
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}
context = {'__name__': '__console__', '__doc__': None, 'mgr': self}
# Enable tab-completion if possible.
self._enable_tab_completion(locs)
self._enable_tab_completion(context)
# 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='')
code.interact(local=context, 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.
# left in limbo with our process thread still running.
pass
except BaseException as exc:
print(f'{Clr.SRED}Unexpected interpreter exception:'
@ -150,7 +152,9 @@ class ServerManagerApp:
raise TypeError(f'Expected a string arg; got {type(statement)}')
with self._process_commands_lock:
self._process_commands.append(statement)
self._block_for_command_completion()
def _block_for_command_completion(self) -> None:
# 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
@ -166,6 +170,29 @@ class ServerManagerApp:
# we'll hopefully still give it enough time to process/print.
time.sleep(0.1)
def broadcast(self, message: str) -> None:
"""Broadcast a message to all connected clients."""
from bacommon.servermanager import BroadcastCommand
self._enqueue_server_command(BroadcastCommand(message=message))
def clientlist(self) -> None:
"""Print a list of connected clients."""
from bacommon.servermanager import ClientListCommand
self._enqueue_server_command(ClientListCommand())
self._block_for_command_completion()
def kick(self, client_id: int, ban_time: Optional[int] = None) -> None:
"""Kick the client with the provided id.
If ban_time is provided, the client will be banned for that
length of time in seconds. If it is None, ban duration will
be determined automatically. Pass 0 or a negative number for no
ban time.
"""
from bacommon.servermanager import KickCommand
self._enqueue_server_command(
KickCommand(client_id=client_id, ban_time=ban_time))
def restart(self, immediate: bool = False) -> None:
"""Restart the server child-process.
@ -360,10 +387,15 @@ class ServerManagerApp:
print(f'{Clr.SMAG}Server process stopped.{Clr.RST}')
if __name__ == '__main__':
def main() -> None:
"""Run a BallisticaCore server manager in interactive mode."""
try:
ServerManagerApp().run_interactive()
except CleanError as clean_exc:
except CleanError as exc:
# For clean errors, do a simple print and fail; no tracebacks/etc.
clean_exc.pretty_print()
exc.pretty_print()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -3988,11 +3988,17 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
</p>
<h3>Methods:</h3>
<h5><a href="#method_ba_ServerController____init__">&lt;constructor&gt;</a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__shutdown">shutdown()</a></h5>
<h5><a href="#method_ba_ServerController____init__">&lt;constructor&gt;</a>, <a href="#method_ba_ServerController__broadcast_message">broadcast_message()</a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__kick">kick()</a>, <a href="#method_ba_ServerController__print_client_list">print_client_list()</a>, <a href="#method_ba_ServerController__shutdown">shutdown()</a></h5>
<dl>
<dt><h4><a name="method_ba_ServerController____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.ServerController(config: ServerConfig)</span></p>
</dd>
<dt><h4><a name="method_ba_ServerController__broadcast_message">broadcast_message()</a></dt></h4><dd>
<p><span>broadcast_message(self, message: str) -&gt; None</span></p>
<p>Broadcast a message to all connected clients.</p>
</dd>
<dt><h4><a name="method_ba_ServerController__handle_transition">handle_transition()</a></dt></h4><dd>
<p><span>handle_transition(self) -&gt; bool</span></p>
@ -4004,6 +4010,22 @@ a good 'end-point' (such as a final score screen).
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__kick">kick()</a></dt></h4><dd>
<p><span>kick(self, client_id: int, ban_time: Optional[int]) -&gt; None</span></p>
<p>Kick the provided client id.</p>
<p>ban_time is provided in seconds.
If ban_time is None, ban duration will be determined automatically.
Pass 0 or a negative number for no ban time.</p>
</dd>
<dt><h4><a name="method_ba_ServerController__print_client_list">print_client_list()</a></dt></h4><dd>
<p><span>print_client_list(self) -&gt; None</span></p>
<p>Print info about all connected clients.</p>
</dd>
<dt><h4><a name="method_ba_ServerController__shutdown">shutdown()</a></dt></h4><dd>
<p><span>shutdown(self, reason: ShutdownReason, immediate: bool) -&gt; None</span></p>

View File

@ -121,3 +121,21 @@ class ShutdownCommand(ServerCommand):
"""Tells the server to shut down."""
reason: ShutdownReason
immediate: bool
@dataclass
class BroadcastCommand(ServerCommand):
"""Broadcast a message to all clients."""
message: str
@dataclass
class ClientListCommand(ServerCommand):
"""Print a list of clients."""
@dataclass
class KickCommand(ServerCommand):
"""Kick a client."""
client_id: int
ban_time: Optional[int]

View File

@ -69,7 +69,7 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None:
for key, value in values.items():
if key not in fieldsdict:
raise AttributeError(
f"'{type(instance).__name__}' has no '{key}' field")
f"'{type(instance).__name__}' has no '{key}' field.")
field = fieldsdict[key]
# We expect to be operating under 'from __future__ import annotations'