mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-30 19:23:20 +08:00
Auto-restart controls and more cleanup on server wrapper
This commit is contained in:
parent
1dc05bed21
commit
ad82e1badf
@ -3932,26 +3932,26 @@
|
||||
"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/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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7a/bc/6c75d6f16c4bd1bd48ae4e31a304",
|
||||
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/61/8a/c805d1785f92c242ca038caeebc9",
|
||||
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9d/88/c37bd934f664e0af630e671a70e9",
|
||||
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c4/07/99095bfc02f73b9b2590a86f8f93",
|
||||
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9d/fb/fba081862fc2e42a5a591c095ef3",
|
||||
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7c/24/71fd9231c09bc0584c92bf10c0d5",
|
||||
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/2b/94679b9fe4df5a3f8ea106a8bb26",
|
||||
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f0/9b/5b1aa55452691590f1e0b00dbbe6",
|
||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/99/8e/0b2157debae0db9e72a9a8224e24",
|
||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8f/18/0b6e28d94eea96e8e2336713515b",
|
||||
"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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/f0/b98326c4e5f54e5fca9e430822bb",
|
||||
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/75/48/8eb219cde7fc49ae97161621178a",
|
||||
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/6e/12/904e655eb8e283aee2604e42080d",
|
||||
"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/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/db/61/fb74c49d524ec0e9eb356f0b0f0a",
|
||||
"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/9c/64/f43f7baf75512685ac0dce8e0f1c",
|
||||
"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/32/c6/4503e9fd3e6d7a27e7e1f32f8542",
|
||||
"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/93/85/c5c09d5fe4e13655ccb021c44d5a",
|
||||
"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/3e/02/5e9b6ffdc3e242e1ddbcc67c027a",
|
||||
"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/1d/37/11d275e939854da3e1488360d1fd",
|
||||
"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/8b/9c/edb852a4d5c7304e39e9618bca93",
|
||||
"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/c7/fc/4e3c1cfce367e5ace09e5bcbe9fc",
|
||||
"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/6e/1c/0d1f8e451d24591b27f15f26a827",
|
||||
"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/70/2a/a1db2d042bbe3c319b2a4bc6c395",
|
||||
"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/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/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",
|
||||
@ -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_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/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0e/3e/d730bb6f8cbd419a6f0bc9ec45b5",
|
||||
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ce/4d/33d705994f4f715b5ef86af3b10c",
|
||||
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/5c/d903596fd08f95bd89c76250e54c",
|
||||
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/aa/bd/f5747ffd8cfbef6970051b1714e1",
|
||||
"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/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/44/14887341014ac31de1e8938ef309"
|
||||
"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/ea/0e/42b6ec781850c69e6bab8e1b14f6",
|
||||
"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/b9/4e/9fe6ea82f278ace93e724253df84",
|
||||
"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/e3/10/4e2baf04c93a5bc632bdbd7e92f6",
|
||||
"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/27/2f/c8895ca79eb249a959578a203ff7"
|
||||
}
|
||||
2
.idea/dictionaries/ericf.xml
generated
2
.idea/dictionaries/ericf.xml
generated
@ -1275,6 +1275,7 @@
|
||||
<w>mathutils</w>
|
||||
<w>maxdepth</w>
|
||||
<w>maxlinks</w>
|
||||
<w>maxtries</w>
|
||||
<w>maxval</w>
|
||||
<w>maxw</w>
|
||||
<w>maxwidth</w>
|
||||
@ -2231,6 +2232,7 @@
|
||||
<w>tret</w>
|
||||
<w>trophystr</w>
|
||||
<w>trsock</w>
|
||||
<w>trynum</w>
|
||||
<w>tscale</w>
|
||||
<w>tscl</w>
|
||||
<w>tself</w>
|
||||
|
||||
@ -175,12 +175,12 @@ class ServerController:
|
||||
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'),
|
||||
color=(1, 0.5, 0.0))
|
||||
print(f'{Clr.SBLU}Exiting for server-restart'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
f' at {timestrval}.{Clr.RST}')
|
||||
else:
|
||||
_ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'),
|
||||
color=(1, 0.5, 0.0))
|
||||
print(f'{Clr.SBLU}Exiting for server-shutdown'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
f' at {timestrval}.{Clr.RST}')
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
|
||||
|
||||
@ -38,7 +38,13 @@ VERSION_STR = '1.2'
|
||||
# 1.2:
|
||||
# Added optional --help arg
|
||||
# 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:
|
||||
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
|
||||
# 1.1.0:
|
||||
@ -62,38 +68,36 @@ class ServerManagerApp:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._config_path = 'config.yaml'
|
||||
self._config = ServerConfig()
|
||||
self._ba_root_path = os.path.abspath('dist/ba_root')
|
||||
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._done = False
|
||||
self._subprocess_commands: List[Union[str, ServerCommand]] = []
|
||||
self._subprocess_commands_lock = Lock()
|
||||
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._subprocess: Optional[subprocess.Popen[bytes]] = None
|
||||
self._launch_time = time.time()
|
||||
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_unclean_exit = False
|
||||
self._subprocess_thread: Optional[Thread] = None
|
||||
|
||||
# If we don't have any explicit exit conditions set,
|
||||
# we run indefinitely (though we restart our subprocess
|
||||
# periodically to clear out leaks/cruft)
|
||||
if (self._config.clean_exit_minutes is None
|
||||
and self._config.unclean_exit_minutes is None
|
||||
and self._config.idle_exit_minutes is None):
|
||||
self._restart_minutes = 360.0
|
||||
# This may override the above defaults.
|
||||
self._parse_command_line_args()
|
||||
|
||||
# Do an initial config-load. If the config is invalid at this point
|
||||
# we can cleanly die (we're more lenient later on reloads).
|
||||
self.load_config(strict=True, print_confirmation=False)
|
||||
|
||||
@property
|
||||
def config(self) -> ServerConfig:
|
||||
@ -106,13 +110,13 @@ class ServerManagerApp:
|
||||
self._config = value
|
||||
|
||||
@property
|
||||
def restart_minutes(self) -> Optional[float]:
|
||||
"""The time between automatic server restarts.
|
||||
def periodic_restart_minutes(self) -> Optional[float]:
|
||||
"""The time between server restarts when running indefinitely.
|
||||
|
||||
Restarting the server periodically can minimize the effect of
|
||||
memory leaks or other built-up cruft.
|
||||
"""
|
||||
return self._restart_minutes
|
||||
return self._periodic_restart_minutes
|
||||
|
||||
def _prerun(self) -> None:
|
||||
"""Common code at the start of any run."""
|
||||
@ -154,6 +158,11 @@ class ServerManagerApp:
|
||||
self._done = True
|
||||
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:
|
||||
"""Do the thing."""
|
||||
if self._interactive:
|
||||
@ -282,10 +291,9 @@ class ServerManagerApp:
|
||||
def restart(self, immediate: bool = True) -> None:
|
||||
"""Restart the server subprocess.
|
||||
|
||||
This can be necessary for some config changes to take effect.
|
||||
By default, the server will exit immediately. If 'immediate' is passed
|
||||
as False, however, the server will instead exit at the next clean
|
||||
transition point (end of a series, etc).
|
||||
By default, the current server process will exit immediately.
|
||||
If 'immediate' is passed as False, however, it will instead exit at
|
||||
the next clean transition point (the end of a series, etc).
|
||||
"""
|
||||
from bacommon.servermanager import ShutdownCommand, ShutdownReason
|
||||
self._enqueue_server_command(
|
||||
@ -299,11 +307,11 @@ class ServerManagerApp:
|
||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT)
|
||||
|
||||
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
|
||||
as False, however, the server will instead exit at the next clean
|
||||
transition point (end of a series, etc).
|
||||
By default, the current server process will exit immediately.
|
||||
If 'immediate' is passed as False, however, it will instead exit at
|
||||
the next clean transition point (the end of a series, etc).
|
||||
"""
|
||||
from bacommon.servermanager import ShutdownCommand, ShutdownReason
|
||||
self._enqueue_server_command(
|
||||
@ -321,6 +329,7 @@ class ServerManagerApp:
|
||||
|
||||
def _parse_command_line_args(self) -> None:
|
||||
"""Parse command line args."""
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
i = 1
|
||||
argc = len(sys.argv)
|
||||
@ -364,11 +373,18 @@ class ServerManagerApp:
|
||||
self._interactive = False
|
||||
did_set_interactive = True
|
||||
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:
|
||||
raise CleanError(f"Invalid arg: '{arg}'.")
|
||||
|
||||
@classmethod
|
||||
def _par(cls, txt: str) -> str:
|
||||
"""Spit out a pretty paragraph for our help text."""
|
||||
import textwrap
|
||||
ind = ' ' * 2
|
||||
out = textwrap.fill(txt, 80, initial_indent=ind, subsequent_indent=ind)
|
||||
@ -388,17 +404,19 @@ class ServerManagerApp:
|
||||
f' Show this help.\n'
|
||||
f'\n'
|
||||
f'{Clr.BLD}--config [path]{Clr.RST}\n' + cls._par(
|
||||
'Set the config file read by the server script. This should'
|
||||
' be in yaml format. Note that yaml is backwards compatible'
|
||||
' with json so you can just write json if you want to. If'
|
||||
' not specified, the script will look for a file named'
|
||||
' \'config.yaml\' in the same directory as the script.') + '\n'
|
||||
'Set the config file read by the server script. The config'
|
||||
' file contains most options for what kind of game to host.'
|
||||
' It should be in yaml format. Note that yaml is backwards'
|
||||
' compatible with json so you can just write json if you'
|
||||
' 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(
|
||||
'Set the ballistica root directory. This is where the game'
|
||||
' will read and write its caches, state files, downloaded'
|
||||
' assets, etc. It needs to be a writable directory. If not'
|
||||
' specified, the script will use the \'dist/ba_root\''
|
||||
' directory relative to itself.') + '\n'
|
||||
'Set the ballistica root directory. This is where the server'
|
||||
' binary will read and write its caches, state files,'
|
||||
' downloaded assets, etc. It needs to be a writable'
|
||||
' directory. If not specified, the script will use the'
|
||||
' \'dist/ba_root\' directory relative to itself.') + '\n'
|
||||
f'{Clr.BLD}--interactive{Clr.RST}\n'
|
||||
f'{Clr.BLD}--noninteractive{Clr.RST}\n' + cls._par(
|
||||
'Specify whether the script should run interactively.'
|
||||
@ -408,28 +426,86 @@ class ServerManagerApp:
|
||||
'end-of-file is reached in stdin. Noninteractive mode creates'
|
||||
' no interpreter and is more suited to being run in automated'
|
||||
' 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)
|
||||
|
||||
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):
|
||||
import yaml
|
||||
with open(self._config_path) as infile:
|
||||
user_config_raw = yaml.safe_load(infile.read())
|
||||
If strict is True, errors will propagate upward.
|
||||
Otherwise, warnings will be printed and repeated attempts will be
|
||||
made to load the config. Eventually the function will give up
|
||||
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.
|
||||
if user_config_raw is not None:
|
||||
return dataclass_from_dict(ServerConfig, user_config_raw)
|
||||
else:
|
||||
print(
|
||||
f"Warning: config file not found at '{self._config_path}'"
|
||||
f'; will use default config.',
|
||||
file=sys.stderr,
|
||||
flush=True)
|
||||
time.sleep(1)
|
||||
|
||||
for _j in range(retry_seconds):
|
||||
# If the app is trying to die, drop what we're doing.
|
||||
if self._done:
|
||||
return
|
||||
time.sleep(1)
|
||||
|
||||
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.
|
||||
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:
|
||||
"""Enable tab-completion on platforms where available (linux/mac)."""
|
||||
@ -455,6 +531,11 @@ class ServerManagerApp:
|
||||
def _run_server_cycle(self) -> None:
|
||||
"""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()
|
||||
|
||||
# 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
|
||||
# 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:
|
||||
self._done = True
|
||||
|
||||
@ -572,76 +654,109 @@ class ServerManagerApp:
|
||||
self._request_shutdowns_or_restarts()
|
||||
|
||||
# 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
|
||||
and time.time() > self._subprocess_force_kill_time):
|
||||
print(f'{Clr.CYN}Force-killing subprocess...{Clr.RST}')
|
||||
break
|
||||
|
||||
# Watch for the process exiting on its own..
|
||||
# Watch for the server process exiting..
|
||||
code: Optional[int] = self._subprocess.poll()
|
||||
if code is not None:
|
||||
if code == 0:
|
||||
clr = Clr.CYN
|
||||
slp = 0.0
|
||||
desc = ''
|
||||
elif code == 154:
|
||||
clr = Clr.CYN
|
||||
slp = 0.0
|
||||
desc = ' (idle_exit_minutes reached)'
|
||||
|
||||
# If they don't want auto-restart, exit the whole wrapper.
|
||||
# (and make sure to exit with an error code if things ended
|
||||
# badly here).
|
||||
if not self._auto_restart:
|
||||
self._wrapper_shutdown_desired = True
|
||||
else:
|
||||
clr = Clr.SRED
|
||||
slp = 5.0 # Avoid super fast death loops.
|
||||
desc = ''
|
||||
if code != 0:
|
||||
self._should_report_subprocess_error = True
|
||||
|
||||
clr = Clr.CYN if code == 0 else Clr.RED
|
||||
print(f'{clr}Server subprocess exited'
|
||||
f' with code {code}{desc}.{Clr.RST}')
|
||||
f' with code {code}.{Clr.RST}')
|
||||
self._reset_subprocess_vars()
|
||||
time.sleep(slp)
|
||||
|
||||
# Avoid super fast death loops.
|
||||
if code != 0 and self._auto_restart:
|
||||
time.sleep(5.0)
|
||||
break
|
||||
|
||||
time.sleep(0.25)
|
||||
|
||||
def _request_shutdowns_or_restarts(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
assert current_thread() is self._subprocess_thread
|
||||
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 >
|
||||
(self._restart_minutes * 60.0)
|
||||
and not self._subprocess_sent_auto_restart):
|
||||
print(f'{Clr.CYN}restart_minutes ({self._restart_minutes})'
|
||||
f' elapsed; requesting subprocess'
|
||||
f' soft restart...{Clr.RST}')
|
||||
self.restart()
|
||||
self._subprocess_sent_auto_restart = True
|
||||
# If we're doing auto-restarts, restart periodically to freshen up.
|
||||
if (self._auto_restart and sincelaunch >
|
||||
(self._periodic_restart_minutes * 60.0)
|
||||
and not self._subprocess_sent_periodic_restart):
|
||||
print(f'{Clr.CYN}periodic_restart_minutes'
|
||||
f' ({self._periodic_restart_minutes})'
|
||||
f' elapsed; requesting soft'
|
||||
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:
|
||||
elapsed = (time.time() - self._launch_time) / 60.0
|
||||
if (elapsed > self._config.clean_exit_minutes
|
||||
and not self._subprocess_sent_clean_exit):
|
||||
opname = 'restart' if self._auto_restart else 'shutdown'
|
||||
print(f'{Clr.CYN}clean_exit_minutes'
|
||||
f' ({self._config.clean_exit_minutes})'
|
||||
f' elapsed; requesting subprocess'
|
||||
f' shutdown...{Clr.RST}')
|
||||
self.shutdown(immediate=False)
|
||||
f' elapsed; requesting immediate'
|
||||
f' {opname}.{Clr.RST}')
|
||||
if self._auto_restart:
|
||||
self.restart(immediate=False)
|
||||
else:
|
||||
self.shutdown(immediate=False)
|
||||
self._subprocess_sent_clean_exit = True
|
||||
|
||||
# Attempt unclean exit if our unclean-exit-time passes.
|
||||
if self._config.unclean_exit_minutes is not None:
|
||||
elapsed = (time.time() - self._launch_time) / 60.0
|
||||
if (elapsed > self._config.unclean_exit_minutes
|
||||
and not self._subprocess_sent_unclean_exit):
|
||||
opname = 'restart' if self._auto_restart else 'shutdown'
|
||||
print(f'{Clr.CYN}unclean_exit_minutes'
|
||||
f' ({self._config.unclean_exit_minutes})'
|
||||
f' elapsed; requesting subprocess'
|
||||
f' shutdown...{Clr.RST}')
|
||||
self.shutdown(immediate=True)
|
||||
f' elapsed; requesting immediate'
|
||||
f' {opname}.{Clr.RST}')
|
||||
if self._auto_restart:
|
||||
self.restart(immediate=True)
|
||||
else:
|
||||
self.shutdown(immediate=True)
|
||||
self._subprocess_sent_unclean_exit = True
|
||||
|
||||
def _reset_subprocess_vars(self) -> None:
|
||||
self._subprocess = 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_unclean_exit = False
|
||||
self._subprocess_force_kill_time = None
|
||||
|
||||
@ -542,6 +542,7 @@
|
||||
<w>maskhigh</w>
|
||||
<w>maskuv</w>
|
||||
<w>maximus</w>
|
||||
<w>maxtries</w>
|
||||
<w>maxwidth</w>
|
||||
<w>mediump</w>
|
||||
<w>memalign</w>
|
||||
@ -946,6 +947,7 @@
|
||||
<w>trilinear</w>
|
||||
<w>trimesh</w>
|
||||
<w>trimeshes</w>
|
||||
<w>trynum</w>
|
||||
<w>tself</w>
|
||||
<w>tval</w>
|
||||
<w>tvos</w>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- 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,
|
||||
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>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't change here.
|
||||
const int kAppBuildNumber = 20307;
|
||||
const int kAppBuildNumber = 20308;
|
||||
const char* kAppVersion = "1.6.0";
|
||||
|
||||
// Our standalone globals.
|
||||
|
||||
@ -508,9 +508,9 @@ void Game::HandleQuitOnIdle() {
|
||||
PushCall([this, idle_seconds] {
|
||||
assert(InGameThread());
|
||||
|
||||
// Special exit value the wrapper script looks for to know we
|
||||
// idled out.
|
||||
g_app_globals->return_value = 154;
|
||||
// Special exit value the wrapper script looks for to know we idled out.
|
||||
// UPDATE: no longer need this.
|
||||
// g_app_globals->return_value = 154;
|
||||
|
||||
// Just go through _ba.quit()
|
||||
// FIXME: Shouldn't need to go out to the python layer here...
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user