Auto-restart controls and more cleanup on server wrapper

This commit is contained in:
Eric Froemling 2021-02-25 14:05:20 -06:00
parent 1dc05bed21
commit ad82e1badf
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
8 changed files with 241 additions and 122 deletions

View File

@ -3932,26 +3932,26 @@
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450",
"assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e",
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
"build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9b/14/177c2689a9f00af6d64347a4b985", "build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/61/c1/35c60d99bf4867eada2b7ad0dc19",
"build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7a/bc/6c75d6f16c4bd1bd48ae4e31a304", "build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/64/f43f7baf75512685ac0dce8e0f1c",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/63/98/38d14bf5bbdf01d898bf157e9491", "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ce/75/e25f75557b05b0de2ab4a281c0de",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/61/8a/c805d1785f92c242ca038caeebc9", "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/32/c6/4503e9fd3e6d7a27e7e1f32f8542",
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9d/88/c37bd934f664e0af630e671a70e9", "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/6b/b7/47e4719f2aef688e62ad2f416406",
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c4/07/99095bfc02f73b9b2590a86f8f93", "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/93/85/c5c09d5fe4e13655ccb021c44d5a",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6c/a9/22b68d2b6e54bd9cd69fa59cc4e6", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/42/7f/46fd7611f88f659d27e827a58c8f",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9d/fb/fba081862fc2e42a5a591c095ef3", "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/02/5e9b6ffdc3e242e1ddbcc67c027a",
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7c/24/71fd9231c09bc0584c92bf10c0d5", "build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/dc/6d/0a0a41762f27f0f8cdd2a7d2c033",
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/2b/94679b9fe4df5a3f8ea106a8bb26", "build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/1d/37/11d275e939854da3e1488360d1fd",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0f/f3/5b7f71d5f98d90444f2c1781776c", "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/63/26/e9e64910b7e242129c5006234116",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f0/9b/5b1aa55452691590f1e0b00dbbe6", "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8b/9c/edb852a4d5c7304e39e9618bca93",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/99/8e/0b2157debae0db9e72a9a8224e24", "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/22/fb/29e80f93d3899abc57854b3dbe52",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8f/18/0b6e28d94eea96e8e2336713515b", "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/fc/4e3c1cfce367e5ace09e5bcbe9fc",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fa/60/b97a279322a881d860adac050c04", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9f/ee/ec4ed4396a600c69202566f2713d",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/f0/b98326c4e5f54e5fca9e430822bb", "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6e/1c/0d1f8e451d24591b27f15f26a827",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/75/48/8eb219cde7fc49ae97161621178a", "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d1/79/d3cb45d123726fac04611ce103f9",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/6e/12/904e655eb8e283aee2604e42080d", "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/70/2a/a1db2d042bbe3c319b2a4bc6c395",
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1a/2c/2407f7b2b2f756215d26fd713d51", "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f5/8c/bd7cb4286e0dadf7b29211a95560",
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/db/61/fb74c49d524ec0e9eb356f0b0f0a", "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/4c/62/5797c0590d0128637c419f74cb67",
"build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/bf/393694ea67f3d590dd2706c9955e", "build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/bf/393694ea67f3d590dd2706c9955e",
"build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/78/cb/bb9ae4f896f862074057c8e36e1d", "build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/78/cb/bb9ae4f896f862074057c8e36e1d",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/bd/39d7b885f7f01e81d0e96f0f85ce", "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/bd/39d7b885f7f01e81d0e96f0f85ce",
@ -3960,12 +3960,12 @@
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/52/d9/563a6949d2c4db5a915c54460fbc", "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/52/d9/563a6949d2c4db5a915c54460fbc",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/6a/42fe8d2e34f95e1b3282e8422344", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/6a/42fe8d2e34f95e1b3282e8422344",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/cf/bad44b07a4022aee3001002086b5", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/cf/bad44b07a4022aee3001002086b5",
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0e/3e/d730bb6f8cbd419a6f0bc9ec45b5", "build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bd/bc/1e305f3aafb660b0f45256fd07d1",
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ce/4d/33d705994f4f715b5ef86af3b10c", "build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ea/0e/42b6ec781850c69e6bab8e1b14f6",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cc/21/6a8ba5a374899e96de6842b456dc", "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/92/84/7e78c73d0c91d45f92f795155d5f",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/5c/d903596fd08f95bd89c76250e54c", "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/4e/9fe6ea82f278ace93e724253df84",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f9/25/6cbd4a35e2d75a61cdb379a7148b", "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/a4/7e2d2dbc870b286358d34b6e6cee",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/aa/bd/f5747ffd8cfbef6970051b1714e1", "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e3/10/4e2baf04c93a5bc632bdbd7e92f6",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c9/4b/4da5368fe05606c3717a2fadfbaf", "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/23/f4/c55e363bb00319971d21b2382431",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/44/14887341014ac31de1e8938ef309" "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/27/2f/c8895ca79eb249a959578a203ff7"
} }

View File

@ -1275,6 +1275,7 @@
<w>mathutils</w> <w>mathutils</w>
<w>maxdepth</w> <w>maxdepth</w>
<w>maxlinks</w> <w>maxlinks</w>
<w>maxtries</w>
<w>maxval</w> <w>maxval</w>
<w>maxw</w> <w>maxw</w>
<w>maxwidth</w> <w>maxwidth</w>
@ -2231,6 +2232,7 @@
<w>tret</w> <w>tret</w>
<w>trophystr</w> <w>trophystr</w>
<w>trsock</w> <w>trsock</w>
<w>trynum</w>
<w>tscale</w> <w>tscale</w>
<w>tscl</w> <w>tscl</w>
<w>tself</w> <w>tself</w>

View File

@ -175,12 +175,12 @@ class ServerController:
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'), _ba.screenmessage(Lstr(resource='internal.serverRestartingText'),
color=(1, 0.5, 0.0)) color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-restart' print(f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}{Clr.RST}') f' at {timestrval}.{Clr.RST}')
else: else:
_ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'), _ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'),
color=(1, 0.5, 0.0)) color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-shutdown' print(f'{Clr.SBLU}Exiting for server-shutdown'
f' at {timestrval}{Clr.RST}') f' at {timestrval}.{Clr.RST}')
with _ba.Context('ui'): with _ba.Context('ui'):
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL) _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)

View File

@ -38,7 +38,13 @@ VERSION_STR = '1.2'
# 1.2: # 1.2:
# Added optional --help arg # Added optional --help arg
# Added --config arg for setting config path and --root for ba_root path # Added --config arg for setting config path and --root for ba_root path
# Added noninteractive mode and --interactive/--noninteractive to select # Added noninteractive mode and --interactive/--noninteractive args to
# explicitly specify
# 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:
@ -62,38 +68,36 @@ class ServerManagerApp:
def __init__(self) -> None: def __init__(self) -> None:
self._config_path = 'config.yaml' self._config_path = 'config.yaml'
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()
# This may override the above defaults.
self._parse_command_line_args()
try:
self._config = self._load_config()
except Exception as exc:
raise CleanError(f'Error loading config: {exc}') from exc
self._wrapper_shutdown_desired = False self._wrapper_shutdown_desired = False
self._done = False self._done = False
self._subprocess_commands: List[Union[str, ServerCommand]] = [] self._subprocess_commands: List[Union[str, ServerCommand]] = []
self._subprocess_commands_lock = Lock() self._subprocess_commands_lock = Lock()
self._subprocess_force_kill_time: Optional[float] = None self._subprocess_force_kill_time: Optional[float] = None
self._restart_minutes: Optional[float] = None self._auto_restart = True
self._config_auto_restart = True
self._config_mtime: Optional[float] = None
self._last_config_mtime_check_time: Optional[float] = None
self._should_report_subprocess_error = False
self._periodic_restart_minutes = 360.0
self._running = False self._running = False
self._subprocess: Optional[subprocess.Popen[bytes]] = None self._subprocess: Optional[subprocess.Popen[bytes]] = None
self._launch_time = time.time() self._launch_time = time.time()
self._subprocess_launch_time: Optional[float] = None self._subprocess_launch_time: Optional[float] = None
self._subprocess_sent_auto_restart = False self._subprocess_sent_periodic_restart = False
self._subprocess_sent_config_auto_restart = False
self._subprocess_sent_clean_exit = False self._subprocess_sent_clean_exit = False
self._subprocess_sent_unclean_exit = False self._subprocess_sent_unclean_exit = False
self._subprocess_thread: Optional[Thread] = None self._subprocess_thread: Optional[Thread] = None
# If we don't have any explicit exit conditions set, # This may override the above defaults.
# we run indefinitely (though we restart our subprocess self._parse_command_line_args()
# periodically to clear out leaks/cruft)
if (self._config.clean_exit_minutes is None # Do an initial config-load. If the config is invalid at this point
and self._config.unclean_exit_minutes is None # we can cleanly die (we're more lenient later on reloads).
and self._config.idle_exit_minutes is None): self.load_config(strict=True, print_confirmation=False)
self._restart_minutes = 360.0
@property @property
def config(self) -> ServerConfig: def config(self) -> ServerConfig:
@ -106,13 +110,13 @@ class ServerManagerApp:
self._config = value self._config = value
@property @property
def restart_minutes(self) -> Optional[float]: def periodic_restart_minutes(self) -> Optional[float]:
"""The time between automatic server restarts. """The time between server restarts when running indefinitely.
Restarting the server periodically can minimize the effect of Restarting the server periodically can minimize the effect of
memory leaks or other built-up cruft. memory leaks or other built-up cruft.
""" """
return self._restart_minutes return self._periodic_restart_minutes
def _prerun(self) -> None: def _prerun(self) -> None:
"""Common code at the start of any run.""" """Common code at the start of any run."""
@ -154,6 +158,11 @@ class ServerManagerApp:
self._done = True self._done = True
self._subprocess_thread.join() self._subprocess_thread.join()
# If there's a server error we should care about, exit the
# entire wrapper uncleanly.
if self._should_report_subprocess_error:
raise CleanError('Server subprocess exited uncleanly.')
def run(self) -> None: def run(self) -> None:
"""Do the thing.""" """Do the thing."""
if self._interactive: if self._interactive:
@ -282,10 +291,9 @@ class ServerManagerApp:
def restart(self, immediate: bool = True) -> None: def restart(self, immediate: bool = True) -> None:
"""Restart the server subprocess. """Restart the server subprocess.
This can be necessary for some config changes to take effect. By default, the current server process will exit immediately.
By default, the server will exit immediately. If 'immediate' is passed If 'immediate' is passed as False, however, it will instead exit at
as False, however, the server will instead exit at the next clean the next clean transition point (the end of a series, etc).
transition point (end of a series, etc).
""" """
from bacommon.servermanager import ShutdownCommand, ShutdownReason from bacommon.servermanager import ShutdownCommand, ShutdownReason
self._enqueue_server_command( self._enqueue_server_command(
@ -299,11 +307,11 @@ class ServerManagerApp:
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT) time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT)
def shutdown(self, immediate: bool = True) -> None: def shutdown(self, immediate: bool = True) -> None:
"""Shut down the server subprocess and exit the wrapper """Shut down the server subprocess and exit the wrapper.
By default, the server will exit immediately. If 'immediate' is passed By default, the current server process will exit immediately.
as False, however, the server will instead exit at the next clean If 'immediate' is passed as False, however, it will instead exit at
transition point (end of a series, etc). the next clean transition point (the end of a series, etc).
""" """
from bacommon.servermanager import ShutdownCommand, ShutdownReason from bacommon.servermanager import ShutdownCommand, ShutdownReason
self._enqueue_server_command( self._enqueue_server_command(
@ -321,6 +329,7 @@ class ServerManagerApp:
def _parse_command_line_args(self) -> None: def _parse_command_line_args(self) -> None:
"""Parse command line args.""" """Parse command line args."""
# pylint: disable=too-many-branches
i = 1 i = 1
argc = len(sys.argv) argc = len(sys.argv)
@ -364,11 +373,18 @@ class ServerManagerApp:
self._interactive = False self._interactive = False
did_set_interactive = True did_set_interactive = True
i += 1 i += 1
elif arg == '--no-auto-restart':
self._auto_restart = False
i += 1
elif arg == '--no-config-auto-restart':
self._config_auto_restart = False
i += 1
else: else:
raise CleanError(f"Invalid arg: '{arg}'.") raise CleanError(f"Invalid arg: '{arg}'.")
@classmethod @classmethod
def _par(cls, txt: str) -> str: def _par(cls, txt: str) -> str:
"""Spit out a pretty paragraph for our help text."""
import textwrap import textwrap
ind = ' ' * 2 ind = ' ' * 2
out = textwrap.fill(txt, 80, initial_indent=ind, subsequent_indent=ind) out = textwrap.fill(txt, 80, initial_indent=ind, subsequent_indent=ind)
@ -388,17 +404,19 @@ class ServerManagerApp:
f' Show this help.\n' f' Show this help.\n'
f'\n' f'\n'
f'{Clr.BLD}--config [path]{Clr.RST}\n' + cls._par( f'{Clr.BLD}--config [path]{Clr.RST}\n' + cls._par(
'Set the config file read by the server script. This should' 'Set the config file read by the server script. The config'
' be in yaml format. Note that yaml is backwards compatible' ' file contains most options for what kind of game to host.'
' with json so you can just write json if you want to. If' ' It should be in yaml format. Note that yaml is backwards'
' not specified, the script will look for a file named' ' compatible with json so you can just write json if you'
' \'config.yaml\' in the same directory as the script.') + '\n' ' want to. If not specified, the script will look for a'
' file named \'config.yaml\' in the same directory as the'
' script.') + '\n'
f'{Clr.BLD}--root [path]{Clr.RST}\n' + cls._par( f'{Clr.BLD}--root [path]{Clr.RST}\n' + cls._par(
'Set the ballistica root directory. This is where the game' 'Set the ballistica root directory. This is where the server'
' will read and write its caches, state files, downloaded' ' binary will read and write its caches, state files,'
' assets, etc. It needs to be a writable directory. If not' ' downloaded assets, etc. It needs to be a writable'
' specified, the script will use the \'dist/ba_root\'' ' directory. If not specified, the script will use the'
' directory relative to itself.') + '\n' ' \'dist/ba_root\' directory relative to itself.') + '\n'
f'{Clr.BLD}--interactive{Clr.RST}\n' f'{Clr.BLD}--interactive{Clr.RST}\n'
f'{Clr.BLD}--noninteractive{Clr.RST}\n' + cls._par( f'{Clr.BLD}--noninteractive{Clr.RST}\n' + cls._par(
'Specify whether the script should run interactively.' 'Specify whether the script should run interactively.'
@ -408,28 +426,86 @@ class ServerManagerApp:
'end-of-file is reached in stdin. Noninteractive mode creates' 'end-of-file is reached in stdin. Noninteractive mode creates'
' no interpreter and is more suited to being run in automated' ' no interpreter and is more suited to being run in automated'
' scenarios. By default, interactive mode will be used if' ' scenarios. By default, interactive mode will be used if'
' a terminal is detected and noninteractive mode otherwise.')) ' a terminal is detected and noninteractive mode otherwise.') +
'\n'
f'{Clr.BLD}--no-auto-restart{Clr.RST}\n' +
cls._par('Auto-restart is enabled by default, which means the'
' server manager will restart the server binary whenever'
' it exits (even when uncleanly). Disabling auto-restart'
' will instead cause the server manager to exit after a'
' single run, returning an error code if the binary'
' did so.') + '\n'
f'{Clr.BLD}--no-config-auto-restart{Clr.RST}\n' + cls._par(
'By default, when auto-restart is enabled, the server binary'
' will be automatically restarted if changes to the server'
' config file are detected. This disables that behavior.'))
print(out) print(out)
def _load_config(self) -> ServerConfig: def load_config(self, strict: bool, print_confirmation: bool) -> None:
"""Load the config.
if os.path.exists(self._config_path): If strict is True, errors will propagate upward.
import yaml Otherwise, warnings will be printed and repeated attempts will be
with open(self._config_path) as infile: made to load the config. Eventually the function will give up
user_config_raw = yaml.safe_load(infile.read()) and leave the existing config as-is.
"""
retry_seconds = 3
maxtries = 11
for trynum in range(maxtries):
try:
self._config = self._load_config_from_file(
print_confirmation=print_confirmation)
return
except Exception as exc:
if strict:
raise CleanError(
f'Error loading config file:\n{exc}') from exc
print(f'{Clr.RED}Error loading config file:\n{exc}.{Clr.RST}')
if trynum == maxtries - 1:
print(f'{Clr.RED}Max-tries reached; giving up.'
f' Existing config values will be used.{Clr.RST}')
break
print(
f'{Clr.CYN}Please correct the error.'
f' Will re-attempt load in {retry_seconds}'
f' seconds. (attempt {trynum+1} of {maxtries-1}).{Clr.RST}'
)
# An empty config file will yield None, and that's ok. time.sleep(1)
if user_config_raw is not None:
return dataclass_from_dict(ServerConfig, user_config_raw) for _j in range(retry_seconds):
else: # If the app is trying to die, drop what we're doing.
print( if self._done:
f"Warning: config file not found at '{self._config_path}'" return
f'; will use default config.', time.sleep(1)
file=sys.stderr,
flush=True) def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig:
out: Optional[ServerConfig] = None
if not os.path.exists(self._config_path):
raise RuntimeError(
f"Config file not found: '{self._config_path}'.")
import yaml
with open(self._config_path) as infile:
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.
self._config_mtime = Path(self._config_path).stat().st_mtime
self._last_config_mtime_check_time = time.time()
# Go with defaults if we weren't able to load anything. # Go with defaults if we weren't able to load anything.
return ServerConfig() if out is None:
out = ServerConfig()
if print_confirmation:
print(f'{Clr.CYN}Valid server config file loaded.{Clr.RST}')
return out
def _enable_tab_completion(self, locs: Dict) -> None: def _enable_tab_completion(self, locs: Dict) -> None:
"""Enable tab-completion on platforms where available (linux/mac).""" """Enable tab-completion on platforms where available (linux/mac)."""
@ -455,6 +531,11 @@ class ServerManagerApp:
def _run_server_cycle(self) -> None: def _run_server_cycle(self) -> None:
"""Spin up the server subprocess and run it until exit.""" """Spin up the server subprocess and run it until exit."""
# Reload our config, and update our overall behavior based on it.
# We do non-strict this time to give the user repeated attempts if
# if they mess up while modifying the config on the fly.
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;
@ -488,6 +569,7 @@ class ServerManagerApp:
# 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 os.kill while main thread is blocked in Thread.join)
if not self._done: if not self._done:
self._done = True self._done = True
@ -572,76 +654,109 @@ class ServerManagerApp:
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 this
# loop; the cleanup code will kill the process. # loop; the cleanup code will kill the process if its still
# alive.
if (self._subprocess_force_kill_time is not None if (self._subprocess_force_kill_time is not None
and time.time() > self._subprocess_force_kill_time): and time.time() > self._subprocess_force_kill_time):
print(f'{Clr.CYN}Force-killing subprocess...{Clr.RST}') print(f'{Clr.CYN}Force-killing subprocess...{Clr.RST}')
break break
# Watch for the process exiting on its own.. # Watch for the server process exiting..
code: Optional[int] = self._subprocess.poll() code: Optional[int] = self._subprocess.poll()
if code is not None: if code is not None:
if code == 0:
clr = Clr.CYN # If they don't want auto-restart, exit the whole wrapper.
slp = 0.0 # (and make sure to exit with an error code if things ended
desc = '' # badly here).
elif code == 154: if not self._auto_restart:
clr = Clr.CYN
slp = 0.0
desc = ' (idle_exit_minutes reached)'
self._wrapper_shutdown_desired = True self._wrapper_shutdown_desired = True
else: if code != 0:
clr = Clr.SRED self._should_report_subprocess_error = True
slp = 5.0 # Avoid super fast death loops.
desc = '' clr = Clr.CYN if code == 0 else Clr.RED
print(f'{clr}Server subprocess exited' print(f'{clr}Server subprocess exited'
f' with code {code}{desc}.{Clr.RST}') f' with code {code}.{Clr.RST}')
self._reset_subprocess_vars() self._reset_subprocess_vars()
time.sleep(slp)
# Avoid super fast death loops.
if code != 0 and self._auto_restart:
time.sleep(5.0)
break break
time.sleep(0.25) time.sleep(0.25)
def _request_shutdowns_or_restarts(self) -> None: def _request_shutdowns_or_restarts(self) -> None:
# pylint: disable=too-many-branches
assert current_thread() is self._subprocess_thread assert current_thread() is self._subprocess_thread
assert self._subprocess_launch_time is not None assert self._subprocess_launch_time is not None
sincelaunch = time.time() - self._subprocess_launch_time now = time.time()
sincelaunch = now - self._subprocess_launch_time
if (self._restart_minutes is not None and sincelaunch > # If we're doing auto-restarts, restart periodically to freshen up.
(self._restart_minutes * 60.0) if (self._auto_restart and sincelaunch >
and not self._subprocess_sent_auto_restart): (self._periodic_restart_minutes * 60.0)
print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})' and not self._subprocess_sent_periodic_restart):
f' elapsed; requesting subprocess' print(f'{Clr.CYN}periodic_restart_minutes'
f' soft restart...{Clr.RST}') f' ({self._periodic_restart_minutes})'
self.restart() f' elapsed; requesting soft'
self._subprocess_sent_auto_restart = True f' restart.{Clr.RST}')
self.restart(immediate=False)
self._subprocess_sent_periodic_restart = True
# If we're doing auto-restart with config changes, handle that.
if (self._auto_restart and self._config_auto_restart
and not self._subprocess_sent_config_auto_restart):
if (self._last_config_mtime_check_time is None
or (now - self._last_config_mtime_check_time) > 3.123):
self._last_config_mtime_check_time = now
mtime: Optional[float]
if os.path.isfile(self._config_path):
mtime = Path(self._config_path).stat().st_mtime
else:
mtime = None
if mtime != self._config_mtime:
print(f'{Clr.CYN}Config-file change detected;'
f' requesting immediate restart.{Clr.RST}')
self.restart(immediate=True)
self._subprocess_sent_config_auto_restart = True
# Attempt clean exit if our clean-exit-time passes.
if self._config.clean_exit_minutes is not None: if self._config.clean_exit_minutes is not None:
elapsed = (time.time() - self._launch_time) / 60.0 elapsed = (time.time() - self._launch_time) / 60.0
if (elapsed > self._config.clean_exit_minutes if (elapsed > self._config.clean_exit_minutes
and not self._subprocess_sent_clean_exit): and not self._subprocess_sent_clean_exit):
opname = 'restart' if self._auto_restart else 'shutdown'
print(f'{Clr.CYN}clean_exit_minutes' print(f'{Clr.CYN}clean_exit_minutes'
f' ({self._config.clean_exit_minutes})' f' ({self._config.clean_exit_minutes})'
f' elapsed; requesting subprocess' f' elapsed; requesting immediate'
f' shutdown...{Clr.RST}') f' {opname}.{Clr.RST}')
self.shutdown(immediate=False) if self._auto_restart:
self.restart(immediate=False)
else:
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.
if self._config.unclean_exit_minutes is not None: if self._config.unclean_exit_minutes is not None:
elapsed = (time.time() - self._launch_time) / 60.0 elapsed = (time.time() - self._launch_time) / 60.0
if (elapsed > self._config.unclean_exit_minutes if (elapsed > self._config.unclean_exit_minutes
and not self._subprocess_sent_unclean_exit): and not self._subprocess_sent_unclean_exit):
opname = 'restart' if self._auto_restart else 'shutdown'
print(f'{Clr.CYN}unclean_exit_minutes' print(f'{Clr.CYN}unclean_exit_minutes'
f' ({self._config.unclean_exit_minutes})' f' ({self._config.unclean_exit_minutes})'
f' elapsed; requesting subprocess' f' elapsed; requesting immediate'
f' shutdown...{Clr.RST}') f' {opname}.{Clr.RST}')
self.shutdown(immediate=True) if self._auto_restart:
self.restart(immediate=True)
else:
self.shutdown(immediate=True)
self._subprocess_sent_unclean_exit = True self._subprocess_sent_unclean_exit = True
def _reset_subprocess_vars(self) -> None: def _reset_subprocess_vars(self) -> None:
self._subprocess = None self._subprocess = None
self._subprocess_launch_time = None self._subprocess_launch_time = None
self._subprocess_sent_auto_restart = False self._subprocess_sent_periodic_restart = False
self._subprocess_sent_config_auto_restart = False
self._subprocess_sent_clean_exit = False self._subprocess_sent_clean_exit = False
self._subprocess_sent_unclean_exit = False self._subprocess_sent_unclean_exit = False
self._subprocess_force_kill_time = None self._subprocess_force_kill_time = None

View File

@ -542,6 +542,7 @@
<w>maskhigh</w> <w>maskhigh</w>
<w>maskuv</w> <w>maskuv</w>
<w>maximus</w> <w>maximus</w>
<w>maxtries</w>
<w>maxwidth</w> <w>maxwidth</w>
<w>mediump</w> <w>mediump</w>
<w>memalign</w> <w>memalign</w>
@ -946,6 +947,7 @@
<w>trilinear</w> <w>trilinear</w>
<w>trimesh</w> <w>trimesh</w>
<w>trimeshes</w> <w>trimeshes</w>
<w>trynum</w>
<w>tself</w> <w>tself</w>
<w>tval</w> <w>tval</w>
<w>tvos</w> <w>tvos</w>

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-02-25 for Ballistica version 1.6.0 build 20306</em></h4> <h4><em>last updated on 2021-02-25 for Ballistica version 1.6.0 build 20307</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p> which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr> <hr>

View File

@ -21,7 +21,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't change here. // These are set automatically via script; don't change here.
const int kAppBuildNumber = 20307; const int kAppBuildNumber = 20308;
const char* kAppVersion = "1.6.0"; const char* kAppVersion = "1.6.0";
// Our standalone globals. // Our standalone globals.

View File

@ -508,9 +508,9 @@ void Game::HandleQuitOnIdle() {
PushCall([this, idle_seconds] { PushCall([this, idle_seconds] {
assert(InGameThread()); assert(InGameThread());
// Special exit value the wrapper script looks for to know we // Special exit value the wrapper script looks for to know we idled out.
// idled out. // UPDATE: no longer need this.
g_app_globals->return_value = 154; // g_app_globals->return_value = 154;
// Just go through _ba.quit() // Just go through _ba.quit()
// FIXME: Shouldn't need to go out to the python layer here... // FIXME: Shouldn't need to go out to the python layer here...