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/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"
}

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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...