diff --git a/.efrocachemap b/.efrocachemap
index 18eae297..c0efec69 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -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"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 072dd990..03a9aeb3 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -1275,6 +1275,7 @@
mathutils
maxdepth
maxlinks
+ maxtries
maxval
maxw
maxwidth
@@ -2231,6 +2232,7 @@
tret
trophystr
trsock
+ trynum
tscale
tscl
tself
diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py
index a0d74209..a4decb9b 100644
--- a/assets/src/ba_data/python/ba/_servermode.py
+++ b/assets/src/ba_data/python/ba/_servermode.py
@@ -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)
diff --git a/assets/src/server/ballisticacore_server.py b/assets/src/server/ballisticacore_server.py
index 73c849b5..c81a0527 100755
--- a/assets/src/server/ballisticacore_server.py
+++ b/assets/src/server/ballisticacore_server.py
@@ -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
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index c0cb3e3c..b1e6c3ad 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -542,6 +542,7 @@
maskhigh
maskuv
maximus
+ maxtries
maxwidth
mediump
memalign
@@ -946,6 +947,7 @@
trilinear
trimesh
trimeshes
+ trynum
tself
tval
tvos
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 3acb187c..5406c71a 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,5 +1,5 @@
-
last updated on 2021-02-25 for Ballistica version 1.6.0 build 20306
+last updated on 2021-02-25 for Ballistica version 1.6.0 build 20307
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 let me know. Happy modding!
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index 7e478300..14814ddd 100644
--- a/src/ballistica/ballistica.cc
+++ b/src/ballistica/ballistica.cc
@@ -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.
diff --git a/src/ballistica/game/game.cc b/src/ballistica/game/game.cc
index 0200f692..b3db38e6 100644
--- a/src/ballistica/game/game.cc
+++ b/src/ballistica/game/game.cc
@@ -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...