mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-14 17:46:03 +00:00
updating headless
This commit is contained in:
parent
1119710804
commit
7d21296d63
2 changed files with 194 additions and 119 deletions
313
bombsquad_server
313
bombsquad_server
|
|
@ -1,23 +1,24 @@
|
||||||
#!/usr/bin/env -S python3.11 -O
|
#!/usr/bin/env -S python3.12 -O
|
||||||
# Released under the MIT License. See LICENSE for details.
|
# Released under the MIT License. See LICENSE for details.
|
||||||
#
|
#
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
"""BallisticaKit server manager."""
|
"""BallisticaKit server manager."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import platform
|
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import tomllib
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Lock, Thread, current_thread
|
from threading import Lock, Thread, current_thread
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
# We make use of the bacommon and efro packages as well as site-packages
|
# We make use of the bacommon and efro packages as well as site-packages
|
||||||
# included with our bundled Ballistica dist, so we need to add those paths
|
# included with our bundled Ballistica dist, so we need to add those
|
||||||
# before we import them.
|
# paths before we import them.
|
||||||
sys.path += [
|
sys.path += [
|
||||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
|
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
|
||||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
|
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages')),
|
||||||
|
|
@ -32,34 +33,68 @@ if TYPE_CHECKING:
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
from bacommon.servermanager import ServerCommand
|
from bacommon.servermanager import ServerCommand
|
||||||
|
|
||||||
VERSION_STR = '1.3.1'
|
VERSION_STR = '1.3.2'
|
||||||
|
|
||||||
# Version history:
|
# Version history:
|
||||||
|
#
|
||||||
|
# 1.3.2
|
||||||
|
#
|
||||||
|
# - Updated to use Python 3.12.
|
||||||
|
#
|
||||||
|
# - Server config file is now in toml format instead of yaml.
|
||||||
|
#
|
||||||
|
# - Server config can now be set to a .json file OR a .toml file.
|
||||||
|
# By default it will look for 'config.json' and then 'config.toml'
|
||||||
|
# in the same dir as this script.
|
||||||
|
#
|
||||||
# 1.3.1
|
# 1.3.1
|
||||||
# Windows binary is now named BallisticaKitHeadless.exe
|
#
|
||||||
|
# - Windows binary is now named 'BallisticaKitHeadless.exe'.
|
||||||
|
#
|
||||||
# 1.3:
|
# 1.3:
|
||||||
# Added show_tutorial config option
|
#
|
||||||
# Added team_names config option
|
# - Added show_tutorial config option.
|
||||||
# Added team_colors config option
|
#
|
||||||
# Added playlist_inline config option
|
# - Added team_names config option.
|
||||||
|
#
|
||||||
|
# - Added team_colors config option.
|
||||||
|
#
|
||||||
|
# - Added playlist_inline config option.
|
||||||
|
#
|
||||||
# 1.2:
|
# 1.2:
|
||||||
# Added optional --help arg
|
#
|
||||||
# Added --config arg for specifying config file and --root for ba_root path
|
# - Added optional --help arg.
|
||||||
# Added noninteractive mode and --interactive/--noninteractive args to
|
#
|
||||||
# explicitly enable/disable it (it is autodetected by default)
|
# - Added --config arg for specifying config file and --root for
|
||||||
# Added explicit control for auto-restart: --no-auto-restart
|
# ba_root path.
|
||||||
# Config file is now reloaded each time server binary is restarted; no more
|
#
|
||||||
# need to bring down server wrapper to pick up changes
|
# - Added noninteractive mode and --interactive/--noninteractive args
|
||||||
# Now automatically restarts server binary when config file is modified
|
# to explicitly enable/disable it (it is autodetected by default).
|
||||||
# (use --no-config-auto-restart to disable that behavior)
|
#
|
||||||
|
# - Added explicit control for auto-restart: --no-auto-restart.
|
||||||
|
#
|
||||||
|
# - Config file is now reloaded each time server binary is restarted;
|
||||||
|
# no more need to bring down server wrapper to pick up changes.
|
||||||
|
#
|
||||||
|
# - Now automatically restarts server binary when config file is
|
||||||
|
# modified (use --no-config-auto-restart to disable that behavior).
|
||||||
|
#
|
||||||
# 1.1.1:
|
# 1.1.1:
|
||||||
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
|
#
|
||||||
|
# - Switched config reading to use
|
||||||
|
# efro.dataclasses.dataclass_from_dict().
|
||||||
|
#
|
||||||
# 1.1.0:
|
# 1.1.0:
|
||||||
# Added shutdown command
|
#
|
||||||
# Changed restart to default to immediate=True
|
# - Added shutdown command.
|
||||||
# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes
|
#
|
||||||
|
# - Changed restart to default to immediate=True.
|
||||||
|
#
|
||||||
|
# - Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes.
|
||||||
|
#
|
||||||
# 1.0.0:
|
# 1.0.0:
|
||||||
# Initial release
|
#
|
||||||
|
# - Initial release.
|
||||||
|
|
||||||
|
|
||||||
class ServerManagerApp:
|
class ServerManagerApp:
|
||||||
|
|
@ -74,8 +109,7 @@ class ServerManagerApp:
|
||||||
IMMEDIATE_SHUTDOWN_TIME_LIMIT = 5.0
|
IMMEDIATE_SHUTDOWN_TIME_LIMIT = 5.0
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._config_path = 'config.yaml'
|
self._user_provided_config_path: str | None = None
|
||||||
self._user_provided_config_path = False
|
|
||||||
self._config = ServerConfig()
|
self._config = ServerConfig()
|
||||||
self._ba_root_path = os.path.abspath('dist/ba_root')
|
self._ba_root_path = os.path.abspath('dist/ba_root')
|
||||||
self._interactive = sys.stdin.isatty()
|
self._interactive = sys.stdin.isatty()
|
||||||
|
|
@ -98,12 +132,14 @@ class ServerManagerApp:
|
||||||
self._subprocess_sent_unclean_exit = False
|
self._subprocess_sent_unclean_exit = False
|
||||||
self._subprocess_thread: Thread | None = None
|
self._subprocess_thread: Thread | None = None
|
||||||
self._subprocess_exited_cleanly: bool | None = None
|
self._subprocess_exited_cleanly: bool | None = None
|
||||||
|
self._did_multi_config_warning = False
|
||||||
|
|
||||||
# This may override the above defaults.
|
# This may override the above defaults.
|
||||||
self._parse_command_line_args()
|
self._parse_command_line_args()
|
||||||
|
|
||||||
# Do an initial config-load. If the config is invalid at this point
|
# Do an initial config-load. If the config is invalid at this
|
||||||
# we can cleanly die (we're more lenient later on reloads).
|
# point we can cleanly die; we're more resilient later on reload
|
||||||
|
# attempts.
|
||||||
self.load_config(strict=True, print_confirmation=False)
|
self.load_config(strict=True, print_confirmation=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -133,8 +169,9 @@ class ServerManagerApp:
|
||||||
|
|
||||||
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
||||||
# need to register a SIGTERM handler so we have a chance to clean
|
# need to register a SIGTERM handler so we have a chance to clean
|
||||||
# up our subprocess when someone tells us to die. (and avoid
|
# need to register a SIGTERM handler so we have a chance to
|
||||||
# zombie processes)
|
# clean up our subprocess when someone tells us to die. (and
|
||||||
|
# avoid zombie processes)
|
||||||
signal.signal(signal.SIGTERM, self._handle_term_signal)
|
signal.signal(signal.SIGTERM, self._handle_term_signal)
|
||||||
|
|
||||||
# During a run, we make the assumption that cwd is the dir
|
# During a run, we make the assumption that cwd is the dir
|
||||||
|
|
@ -156,7 +193,8 @@ class ServerManagerApp:
|
||||||
f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', flush=True
|
f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}', flush=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark ourselves as shutting down and wait for the process to wrap up.
|
# Mark ourselves as shutting down and wait for the process to
|
||||||
|
# wrap up.
|
||||||
self._done = True
|
self._done = True
|
||||||
self._subprocess_thread.join()
|
self._subprocess_thread.join()
|
||||||
|
|
||||||
|
|
@ -182,9 +220,10 @@ class ServerManagerApp:
|
||||||
# Gracefully bow out if we kill ourself via keyboard.
|
# Gracefully bow out if we kill ourself via keyboard.
|
||||||
pass
|
pass
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# We get this from the builtin quit(), our signal handler, etc.
|
# We get this from the builtin quit(), our signal handler,
|
||||||
# Need to catch this so we can clean up, otherwise we'll be
|
# etc. Need to catch this so we can clean up, otherwise
|
||||||
# left in limbo with our process thread still running.
|
# we'll be left in limbo with our process thread still
|
||||||
|
# running.
|
||||||
pass
|
pass
|
||||||
self._postrun()
|
self._postrun()
|
||||||
|
|
||||||
|
|
@ -208,14 +247,17 @@ class ServerManagerApp:
|
||||||
self._enable_tab_completion(context)
|
self._enable_tab_completion(context)
|
||||||
|
|
||||||
# Now just sit in an interpreter.
|
# Now just sit in an interpreter.
|
||||||
# TODO: make it possible to use IPython if the user has it available.
|
#
|
||||||
|
# TODO: make it possible to use IPython if the user has it
|
||||||
|
# available.
|
||||||
try:
|
try:
|
||||||
self._interpreter_start_time = time.time()
|
self._interpreter_start_time = time.time()
|
||||||
code.interact(local=context, banner='', exitmsg='')
|
code.interact(local=context, banner='', exitmsg='')
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
# We get this from the builtin quit(), our signal handler, etc.
|
# We get this from the builtin quit(), our signal handler,
|
||||||
# Need to catch this so we can clean up, otherwise we'll be
|
# etc. Need to catch this so we can clean up, otherwise
|
||||||
# left in limbo with our process thread still running.
|
# we'll be left in limbo with our process thread still
|
||||||
|
# running.
|
||||||
pass
|
pass
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
print(
|
print(
|
||||||
|
|
@ -239,19 +281,21 @@ class ServerManagerApp:
|
||||||
self._block_for_command_completion()
|
self._block_for_command_completion()
|
||||||
|
|
||||||
def _block_for_command_completion(self) -> None:
|
def _block_for_command_completion(self) -> None:
|
||||||
# Ideally we'd block here until the command was run so our prompt would
|
# Ideally we'd block here until the command was run so our
|
||||||
# print after it's results. We currently don't get any response from
|
# prompt would print after it's results. We currently don't get
|
||||||
# the app so the best we can do is block until our bg thread has sent
|
# any response from the app so the best we can do is block until
|
||||||
# it. In the future we can perhaps add a proper 'command port'
|
# our bg thread has sent it. In the future we can perhaps add a
|
||||||
# interface for proper blocking two way communication.
|
# proper 'command port' interface for proper blocking two way
|
||||||
|
# communication.
|
||||||
while True:
|
while True:
|
||||||
with self._subprocess_commands_lock:
|
with self._subprocess_commands_lock:
|
||||||
if not self._subprocess_commands:
|
if not self._subprocess_commands:
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# One last short delay so if we come out *just* as the command is sent
|
# One last short delay so if we come out *just* as the command
|
||||||
# we'll hopefully still give it enough time to process/print.
|
# is sent we'll hopefully still give it enough time to
|
||||||
|
# process/print.
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def screenmessage(
|
def screenmessage(
|
||||||
|
|
@ -321,8 +365,8 @@ class ServerManagerApp:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# If we're asking for an immediate restart but don't get one within
|
# If we're asking for an immediate restart but don't get one
|
||||||
# the grace period, bring down the hammer.
|
# within the grace period, bring down the hammer.
|
||||||
if immediate:
|
if immediate:
|
||||||
self._subprocess_force_kill_time = (
|
self._subprocess_force_kill_time = (
|
||||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
||||||
|
|
@ -341,12 +385,12 @@ class ServerManagerApp:
|
||||||
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
|
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate)
|
||||||
)
|
)
|
||||||
|
|
||||||
# An explicit shutdown means we know to bail completely once this
|
# An explicit shutdown means we know to bail completely once
|
||||||
# subprocess completes.
|
# this subprocess completes.
|
||||||
self._wrapper_shutdown_desired = True
|
self._wrapper_shutdown_desired = True
|
||||||
|
|
||||||
# If we're asking for an immediate shutdown but don't get one within
|
# If we're asking for an immediate shutdown but don't get one
|
||||||
# the grace period, bring down the hammer.
|
# within the grace period, bring down the hammer.
|
||||||
if immediate:
|
if immediate:
|
||||||
self._subprocess_force_kill_time = (
|
self._subprocess_force_kill_time = (
|
||||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT
|
||||||
|
|
@ -372,16 +416,16 @@ class ServerManagerApp:
|
||||||
raise CleanError(f"Supplied path does not exist: '{path}'.")
|
raise CleanError(f"Supplied path does not exist: '{path}'.")
|
||||||
# We need an abs path because we may be in a different
|
# We need an abs path because we may be in a different
|
||||||
# cwd currently than we will be during the run.
|
# cwd currently than we will be during the run.
|
||||||
self._config_path = os.path.abspath(path)
|
self._user_provided_config_path = os.path.abspath(path)
|
||||||
self._user_provided_config_path = True
|
|
||||||
i += 2
|
i += 2
|
||||||
elif arg == '--root':
|
elif arg == '--root':
|
||||||
if i + 1 >= argc:
|
if i + 1 >= argc:
|
||||||
raise CleanError('Expected a path as next arg.')
|
raise CleanError('Expected a path as next arg.')
|
||||||
path = sys.argv[i + 1]
|
path = sys.argv[i + 1]
|
||||||
# Unlike config_path, this one doesn't have to exist now.
|
# Unlike config_path, this one doesn't have to exist
|
||||||
# We do however need an abs path because we may be in a
|
# now. We do however need an abs path because we may be
|
||||||
# different cwd currently than we will be during the run.
|
# in a different cwd currently than we will be during
|
||||||
|
# the run.
|
||||||
self._ba_root_path = os.path.abspath(path)
|
self._ba_root_path = os.path.abspath(path)
|
||||||
i += 2
|
i += 2
|
||||||
elif arg == '--interactive':
|
elif arg == '--interactive':
|
||||||
|
|
@ -440,11 +484,9 @@ class ServerManagerApp:
|
||||||
+ cls._par(
|
+ cls._par(
|
||||||
'Set the config file read by the server script. The config'
|
'Set the config file read by the server script. The config'
|
||||||
' file contains most options for what kind of game to host.'
|
' file contains most options for what kind of game to host.'
|
||||||
' It should be in yaml format. Note that yaml is backwards'
|
' It should be in toml or json format. If not specified,'
|
||||||
' compatible with json so you can just write json if you'
|
' the script will look for a file named \'config.toml\' or'
|
||||||
' want to. If not specified, the script will look for a'
|
' \'config.json\' in the same directory as the script.'
|
||||||
' file named \'config.yaml\' in the same directory as the'
|
|
||||||
' script.'
|
|
||||||
)
|
)
|
||||||
+ '\n'
|
+ '\n'
|
||||||
f'{Clr.BLD}--root [path]{Clr.RST}\n'
|
f'{Clr.BLD}--root [path]{Clr.RST}\n'
|
||||||
|
|
@ -513,11 +555,6 @@ class ServerManagerApp:
|
||||||
f'{Clr.RED}Error loading config file:\n{exc}.{Clr.RST}',
|
f'{Clr.RED}Error loading config file:\n{exc}.{Clr.RST}',
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
with open(self._ba_root_path + "/mods/defaults/config.yaml", "r") as infile:
|
|
||||||
default_file = infile.read()
|
|
||||||
with open(self._config_path, "w") as outfile:
|
|
||||||
outfile.write(default_file)
|
|
||||||
print("config reset done")
|
|
||||||
if trynum == maxtries - 1:
|
if trynum == maxtries - 1:
|
||||||
print(
|
print(
|
||||||
f'{Clr.RED}Max-tries reached; giving up.'
|
f'{Clr.RED}Max-tries reached; giving up.'
|
||||||
|
|
@ -539,20 +576,51 @@ class ServerManagerApp:
|
||||||
return
|
return
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
def _get_config_path(self) -> str:
|
||||||
|
|
||||||
|
if self._user_provided_config_path is not None:
|
||||||
|
return self._user_provided_config_path
|
||||||
|
|
||||||
|
# Otherwise look for config.toml or config.json in the same dir
|
||||||
|
# as our script. Need to work in abs paths since we may chdir when
|
||||||
|
# we start running.
|
||||||
|
toml_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'config.toml')
|
||||||
|
)
|
||||||
|
toml_exists = os.path.exists(toml_path)
|
||||||
|
json_path = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'config.json')
|
||||||
|
)
|
||||||
|
json_exists = os.path.exists(json_path)
|
||||||
|
|
||||||
|
# Warn if both configs are present.
|
||||||
|
if toml_exists and json_exists and not self._did_multi_config_warning:
|
||||||
|
self._did_multi_config_warning = True
|
||||||
|
print(
|
||||||
|
f'{Clr.YLW}Both config.toml and config.json'
|
||||||
|
f' found; will use json.{Clr.RST}',
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
if json_exists:
|
||||||
|
return json_path
|
||||||
|
return toml_path
|
||||||
|
|
||||||
def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig:
|
def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig:
|
||||||
out: ServerConfig | None = None
|
out: ServerConfig | None = None
|
||||||
|
|
||||||
if not os.path.exists(self._config_path):
|
config_path = self._get_config_path()
|
||||||
|
|
||||||
|
if not os.path.exists(config_path):
|
||||||
# Special case:
|
# Special case:
|
||||||
# If the user didn't specify a particular config file, allow
|
#
|
||||||
# gracefully falling back to defaults if the default one is
|
# If the user didn't provide a config path AND the default
|
||||||
# missing.
|
# config path does not exist, fall back to defaults.
|
||||||
if not self._user_provided_config_path:
|
if not self._user_provided_config_path:
|
||||||
if print_confirmation:
|
if print_confirmation:
|
||||||
print(
|
print(
|
||||||
f'{Clr.YLW}Default config file not found'
|
f'{Clr.YLW}Default config file not found'
|
||||||
f' (\'{self._config_path}\'); using default'
|
f' (\'{config_path}\'); using default'
|
||||||
f' settings.{Clr.RST}',
|
f' config.{Clr.RST}',
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
self._config_mtime = None
|
self._config_mtime = None
|
||||||
|
|
@ -560,25 +628,25 @@ class ServerManagerApp:
|
||||||
return ServerConfig()
|
return ServerConfig()
|
||||||
|
|
||||||
# Don't be so lenient if the user pointed us at one though.
|
# Don't be so lenient if the user pointed us at one though.
|
||||||
raise RuntimeError(f"Config file not found: '{self._config_path}'.")
|
raise RuntimeError(f"Config file not found: '{config_path}'.")
|
||||||
|
|
||||||
import yaml
|
with open(config_path, encoding='utf-8') as infile:
|
||||||
|
if config_path.endswith('.toml'):
|
||||||
|
user_config_raw = tomllib.loads(infile.read())
|
||||||
|
elif config_path.endswith('.json'):
|
||||||
|
user_config_raw = json.loads(infile.read())
|
||||||
|
else:
|
||||||
|
raise CleanError(
|
||||||
|
f"Invalid config file path '{config_path}';"
|
||||||
|
f" path must end with '.toml' or '.json'."
|
||||||
|
)
|
||||||
|
|
||||||
with open(self._config_path, encoding='utf-8') as infile:
|
out = dataclass_from_dict(ServerConfig, user_config_raw)
|
||||||
user_config_raw = yaml.safe_load(infile.read())
|
|
||||||
|
|
||||||
# An empty config file will yield None, and that's ok.
|
|
||||||
if user_config_raw is not None:
|
|
||||||
out = dataclass_from_dict(ServerConfig, user_config_raw)
|
|
||||||
|
|
||||||
# Update our known mod-time since we know it exists.
|
# Update our known mod-time since we know it exists.
|
||||||
self._config_mtime = Path(self._config_path).stat().st_mtime
|
self._config_mtime = Path(config_path).stat().st_mtime
|
||||||
self._last_config_mtime_check_time = time.time()
|
self._last_config_mtime_check_time = time.time()
|
||||||
|
|
||||||
# Go with defaults if we weren't able to load anything.
|
|
||||||
if out is None:
|
|
||||||
out = ServerConfig()
|
|
||||||
|
|
||||||
if print_confirmation:
|
if print_confirmation:
|
||||||
print(
|
print(
|
||||||
f'{Clr.CYN}Valid server config file loaded.{Clr.RST}',
|
f'{Clr.CYN}Valid server config file loaded.{Clr.RST}',
|
||||||
|
|
@ -612,24 +680,26 @@ class ServerManagerApp:
|
||||||
"""Spin up the server subprocess and run it until exit."""
|
"""Spin up the server subprocess and run it until exit."""
|
||||||
# pylint: disable=consider-using-with
|
# pylint: disable=consider-using-with
|
||||||
|
|
||||||
# Reload our config, and update our overall behavior based on it.
|
# Reload our config, and update our overall behavior based on
|
||||||
# We do non-strict this time to give the user repeated attempts if
|
# it. We do non-strict this time to give the user repeated
|
||||||
# if they mess up while modifying the config on the fly.
|
# attempts if if they mess up while modifying the config on the
|
||||||
|
# fly.
|
||||||
self.load_config(strict=False, print_confirmation=True)
|
self.load_config(strict=False, print_confirmation=True)
|
||||||
|
|
||||||
self._prep_subprocess_environment()
|
self._prep_subprocess_environment()
|
||||||
|
|
||||||
# Launch the binary and grab its stdin;
|
# Launch the binary and grab its stdin; we'll use this to feed
|
||||||
# we'll use this to feed it commands.
|
# it commands.
|
||||||
self._subprocess_launch_time = time.time()
|
self._subprocess_launch_time = time.time()
|
||||||
|
|
||||||
# Set an environment var so the server process knows its being
|
# Set an environment var so the server process knows its being
|
||||||
# run under us. This causes it to ignore ctrl-c presses and other
|
# run under us. This causes it to ignore ctrl-c presses and
|
||||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
# other slight behavior tweaks. Hmm; should this be an argument
|
||||||
|
# instead?
|
||||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||||
|
|
||||||
# Set an environment var to change the device name.
|
# Set an environment var to change the device name. Device name
|
||||||
# Device name is used while making connection with master server,
|
# is used while making connection with master server,
|
||||||
# cloud-console recognize us with this name.
|
# cloud-console recognize us with this name.
|
||||||
os.environ['BA_DEVICE_NAME'] = self._config.party_name
|
os.environ['BA_DEVICE_NAME'] = self._config.party_name
|
||||||
|
|
||||||
|
|
@ -671,9 +741,10 @@ class ServerManagerApp:
|
||||||
|
|
||||||
assert self._subprocess_exited_cleanly is not None
|
assert self._subprocess_exited_cleanly is not None
|
||||||
|
|
||||||
# EW: it seems that if we die before the main thread has fully started
|
# EW: it seems that if we die before the main thread has fully
|
||||||
# up the interpreter, its possible that it will not break out of its
|
# started up the interpreter, its possible that it will not
|
||||||
# loop via the usual SystemExit that gets sent when we die.
|
# break out of its loop via the usual SystemExit that gets sent
|
||||||
|
# when we die.
|
||||||
if self._interactive:
|
if self._interactive:
|
||||||
while (
|
while (
|
||||||
self._interpreter_start_time is None
|
self._interpreter_start_time is None
|
||||||
|
|
@ -702,8 +773,8 @@ class ServerManagerApp:
|
||||||
# tell the main thread to die.
|
# tell the main thread to die.
|
||||||
if self._wrapper_shutdown_desired:
|
if self._wrapper_shutdown_desired:
|
||||||
# Only do this if the main thread is not already waiting for
|
# Only do this if the main thread is not already waiting for
|
||||||
# us to die; otherwise it can lead to deadlock.
|
# us to die; otherwise it can lead to deadlock. (we hang in
|
||||||
# (we hang in os.kill while main thread is blocked in Thread.join)
|
# os.kill while main thread is blocked in Thread.join)
|
||||||
if not self._done:
|
if not self._done:
|
||||||
self._done = True
|
self._done = True
|
||||||
|
|
||||||
|
|
@ -729,6 +800,8 @@ class ServerManagerApp:
|
||||||
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
|
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
|
||||||
bincfg['Show Tutorial'] = self._config.show_tutorial
|
bincfg['Show Tutorial'] = self._config.show_tutorial
|
||||||
|
|
||||||
|
if self._config.protocol_version is not None:
|
||||||
|
bincfg['SceneV1 Host Protocol'] = self._config.protocol_version
|
||||||
if self._config.team_names is not None:
|
if self._config.team_names is not None:
|
||||||
bincfg['Custom Team Names'] = self._config.team_names
|
bincfg['Custom Team Names'] = self._config.team_names
|
||||||
elif 'Custom Team Names' in bincfg:
|
elif 'Custom Team Names' in bincfg:
|
||||||
|
|
@ -777,8 +850,8 @@ class ServerManagerApp:
|
||||||
assert current_thread() is self._subprocess_thread
|
assert current_thread() is self._subprocess_thread
|
||||||
assert self._subprocess.stdin is not None
|
assert self._subprocess.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)
|
||||||
self._send_server_command(StartServerModeCommand(self._config))
|
self._send_server_command(StartServerModeCommand(self._config))
|
||||||
|
|
||||||
|
|
@ -790,8 +863,8 @@ class ServerManagerApp:
|
||||||
# Pass along any commands to our process.
|
# Pass along any commands to our process.
|
||||||
with self._subprocess_commands_lock:
|
with self._subprocess_commands_lock:
|
||||||
for incmd in self._subprocess_commands:
|
for incmd in self._subprocess_commands:
|
||||||
# If 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
|
||||||
# in any proper structure.
|
# wrap it in any proper structure.
|
||||||
if isinstance(incmd, str):
|
if isinstance(incmd, str):
|
||||||
self._subprocess.stdin.write((incmd + '\n').encode())
|
self._subprocess.stdin.write((incmd + '\n').encode())
|
||||||
self._subprocess.stdin.flush()
|
self._subprocess.stdin.flush()
|
||||||
|
|
@ -802,9 +875,9 @@ class ServerManagerApp:
|
||||||
# Request restarts/shut-downs for various reasons.
|
# Request restarts/shut-downs for various reasons.
|
||||||
self._request_shutdowns_or_restarts()
|
self._request_shutdowns_or_restarts()
|
||||||
|
|
||||||
# If they want to force-kill our subprocess, simply exit this
|
# If they want to force-kill our subprocess, simply exit
|
||||||
# loop; the cleanup code will kill the process if its still
|
# this loop; the cleanup code will kill the process if its
|
||||||
# alive.
|
# still alive.
|
||||||
if (
|
if (
|
||||||
self._subprocess_force_kill_time is not None
|
self._subprocess_force_kill_time is not None
|
||||||
and time.time() > self._subprocess_force_kill_time
|
and time.time() > self._subprocess_force_kill_time
|
||||||
|
|
@ -850,8 +923,9 @@ class ServerManagerApp:
|
||||||
):
|
):
|
||||||
self._last_config_mtime_check_time = now
|
self._last_config_mtime_check_time = now
|
||||||
mtime: float | None
|
mtime: float | None
|
||||||
if os.path.isfile(self._config_path):
|
config_path = self._get_config_path()
|
||||||
mtime = Path(self._config_path).stat().st_mtime
|
if os.path.isfile(config_path):
|
||||||
|
mtime = Path(config_path).stat().st_mtime
|
||||||
else:
|
else:
|
||||||
mtime = None
|
mtime = None
|
||||||
if mtime != self._config_mtime:
|
if mtime != self._config_mtime:
|
||||||
|
|
@ -863,8 +937,8 @@ class ServerManagerApp:
|
||||||
self.restart(immediate=True)
|
self.restart(immediate=True)
|
||||||
self._subprocess_sent_config_auto_restart = True
|
self._subprocess_sent_config_auto_restart = True
|
||||||
|
|
||||||
# Attempt clean exit if our clean-exit-time passes.
|
# Attempt clean exit if our clean-exit-time passes (and enforce
|
||||||
# (and enforce a 6 hour max if not provided)
|
# a 6 hour max if not provided).
|
||||||
clean_exit_minutes = 360.0
|
clean_exit_minutes = 360.0
|
||||||
if self._config.clean_exit_minutes is not None:
|
if self._config.clean_exit_minutes is not None:
|
||||||
clean_exit_minutes = min(
|
clean_exit_minutes = min(
|
||||||
|
|
@ -889,8 +963,8 @@ class ServerManagerApp:
|
||||||
self.shutdown(immediate=False)
|
self.shutdown(immediate=False)
|
||||||
self._subprocess_sent_clean_exit = True
|
self._subprocess_sent_clean_exit = True
|
||||||
|
|
||||||
# Attempt unclean exit if our unclean-exit-time passes.
|
# Attempt unclean exit if our unclean-exit-time passes (and
|
||||||
# (and enforce a 7 hour max if not provided)
|
# enforce a 7 hour max if not provided).
|
||||||
unclean_exit_minutes = 420.0
|
unclean_exit_minutes = 420.0
|
||||||
if self._config.unclean_exit_minutes is not None:
|
if self._config.unclean_exit_minutes is not None:
|
||||||
unclean_exit_minutes = min(
|
unclean_exit_minutes = min(
|
||||||
|
|
@ -932,8 +1006,8 @@ class ServerManagerApp:
|
||||||
|
|
||||||
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
|
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
|
||||||
|
|
||||||
# First, ask it nicely to die and give it a moment.
|
# First, ask it nicely to die and give it a moment. If that
|
||||||
# If that doesn't work, bring down the hammer.
|
# doesn't work, bring down the hammer.
|
||||||
self._subprocess.terminate()
|
self._subprocess.terminate()
|
||||||
try:
|
try:
|
||||||
self._subprocess.wait(timeout=10)
|
self._subprocess.wait(timeout=10)
|
||||||
|
|
@ -949,8 +1023,9 @@ def main() -> None:
|
||||||
try:
|
try:
|
||||||
ServerManagerApp().run()
|
ServerManagerApp().run()
|
||||||
except CleanError as exc:
|
except CleanError as exc:
|
||||||
# For clean errors, do a simple print and fail; no tracebacks/etc.
|
# For clean errors, do a simple print and fail; no
|
||||||
# Any others will bubble up and give us the usual mess.
|
# tracebacks/etc. Any others will bubble up and give us the
|
||||||
|
# usual mess.
|
||||||
exc.pretty_print()
|
exc.pretty_print()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
|
||||||
BIN
dist/bombsquad_headless
vendored
BIN
dist/bombsquad_headless
vendored
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue