diff --git a/.efrocachemap b/.efrocachemap
index 808538f8..bc3ad06f 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -420,7 +420,7 @@
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/60/ad/38269b7f1c7dc20cb9a506cd0681",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/72/85/d6fc4d16b7081d91fba2850b5b10",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/e9/ae/1d674d0c086eaa0bd1c3b1db0505",
- "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/a3/38/05424b4cfb6e23e902b6dc20b209",
+ "assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/68/22/ee4cff9f9fa011db6a2ed7092d1f",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/5b/cf/4501b151257c3d8d6ee8d0497d14",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503",
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/17/21/7b6371bde52392eb4a38e7c6d55a",
@@ -431,12 +431,12 @@
"assets/build/ba_data/data/languages/dutch.json": "https://files.ballistica.net/cache/ba1/68/93/da8e9874f41a786edf52ba4ccaad",
"assets/build/ba_data/data/languages/english.json": "https://files.ballistica.net/cache/ba1/70/7d/6cbdaf130eaa5c58cffb1f321e3d",
"assets/build/ba_data/data/languages/esperanto.json": "https://files.ballistica.net/cache/ba1/4c/c7/0184b8178869d1a3827a1bfcd5bb",
- "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/c1/3c/2d45627563fbfbbbda2b7e6799e1",
+ "assets/build/ba_data/data/languages/filipino.json": "https://files.ballistica.net/cache/ba1/8f/73/093120ae2241d8f4b899ccda2d75",
"assets/build/ba_data/data/languages/french.json": "https://files.ballistica.net/cache/ba1/25/65/1cb03566e73811fc6e1b841d9072",
"assets/build/ba_data/data/languages/german.json": "https://files.ballistica.net/cache/ba1/ef/e6/d4909f571d7473fd04055728490e",
"assets/build/ba_data/data/languages/gibberish.json": "https://files.ballistica.net/cache/ba1/a5/28/6bf6b15f8359a145cd2e599849f1",
"assets/build/ba_data/data/languages/greek.json": "https://files.ballistica.net/cache/ba1/82/eb/37ff44af76812097f9c98f05c730",
- "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/08/3b/68cea4d16f7020d932829af85323",
+ "assets/build/ba_data/data/languages/hindi.json": "https://files.ballistica.net/cache/ba1/50/e8/837be1324c8128507b3df89b689f",
"assets/build/ba_data/data/languages/hungarian.json": "https://files.ballistica.net/cache/ba1/d8/f2/aa16bc336bd7660cc86c3264bfc4",
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/09/6c/942dd354447772a69ea5cae1d486",
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/91/70/05ad4a6fdbdaa0f471225f7ad317",
@@ -3995,26 +3995,26 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
- "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0a/56/252de9190ee6367ccbf37174783d",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f5/30/29f5a9d9cc5c6f5c76e3058d3621",
- "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/e5/037d736cacd93a4b005cc93e72ad",
- "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/22/04/430aa3457c427f0814058c2b4483",
- "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/93/68/307719e44199480a5ee051d993f5",
- "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9f/50/6f8a60e5375bf651bdebda617249",
- "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cf/03/a5a5748fda33c876fbf3e8261b02",
- "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/82/3e/5725b87a8cc1e90f69bec58c65d5",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/64/16/1589abfd35715bd2aa2915766148",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a8/36/584d685f3bea03753acf7344dfce",
- "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/09/cbb451c2e8f856de61c0eafc5fdc",
- "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b3/c9/9b3e221426dae6a047a893a4eb39",
- "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/61/2e/af3b07614ea2fb60f70b3d3b442a",
- "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/6e/fb/4b6e3e14ae9e329ae2a5c2eaab24",
- "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/3b/f8fc04eefa313d673ee98d20e360",
- "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/83/544e088664612666bbaa6c1ff422",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/79/13/29d322c6e8f7717ec87d5027bb20",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/94/01/19f43fe2ee530d48f31665d22ff0",
- "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/17/e9/d6369d897f3595fbe03202887447",
- "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/07/ff/cd46cba42a67cf31d6454b9eba7d",
+ "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/f6/0fa02f0dd61fe86f030e235cb65d",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/5a/c49738579f58cff159f78330685e",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/79/d970690e3fa5d24e0cdfb5aff646",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/56/8203d51e88d563f373bd73304219",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2b/7a/3eb09023c93e907472043fbccfff",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d0/4f/d139bb2f0a1e4e400dade616f5d3",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/5d/20bd4d3a607a8b3a5d9d9d925146",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/f2/0646e8a3ed1c1ae091bee9628b8e",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/08/e8/064033db53071b97422d21386e8f",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e7/e6/3cc5634143190749753a806a4792",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d1/94/513926fe5432ba722b0e4f119f0c",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ff/7e/884381d6bc009c804496a512beaf",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/be/06/c6f50e95926031616daf349acc64",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/16/95/4a354d9d8faa18f26be3a7c57f63",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f1/39/6da99f25127e5ec62b5586e30378",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d6/17/65c7d490b78d8ae0120e1d254b83",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/de/28/bea57ab25706df395792f06fd08b",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4c/29/17e37dd645bb9d5fb6f85db2b1bd",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/26/22/bbd9535c97eedfc2c18c45e65f9b",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/a6/40/e9517fe39850dca141f0cc086503",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3b/0c/2f4061ab877d415a1c30e0e736db",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/5a/2b0714af254c64954ccfe51c70b3",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1f/ae/c8a885b1a1868b6846b606cdb456",
@@ -4031,14 +4031,14 @@
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/df/efb51d1c226eac613d48e2cbf0b8",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/f6/357fe951c86c9fc5b1b737cd91ae",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/04/17/e2de0ab5df6b938d828e8662ce6d",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/69/dc/6fc1614b2548c6ac76c9e891c2e2",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c2/1b/263c5e001c6891d774d941f0bdfd",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7b/3a/f77ffca8d7c45b859d1e48c1b468",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/88/15/1aa07f986d0bf7dac9a1f39635f2",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/4b/e1/646d3095ab442e0b18d4c0de9689",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/fe/034c116781ddfe6cc89ada030056",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/91/64/10fcd883cf0d15895d72a638e2ad",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/1e/69/bf40bc8defe923cfa6d48cb5dd04",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/34/77/1986ffe869aca7b8aee6b24ea64b",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/af/5430940c906f3d0c6e0983b9a2b9",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/22/84/02b9109e1449f2acca49f4f9b934",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/2b/22/b32d8e18c6a258929f091a14419d",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/a2/08/ecf905f1c6ede831e66e8d84c6f6",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/83/d7/01a034b1d9e2f028cb5f964396d3",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/81/a5/e780126b52d530cce18a64ae65e4",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/d8/39/51a851a77b6ce36073e9d190b9bf",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7",
- "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/aa/a5/3ddc86d1789b2bf1d376b7671a3d"
+ "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/98/12/571b2160d69d42580e8f31fa6a8d"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index a679acc7..ab5c5eba 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -337,6 +337,7 @@
capturetheflag
carentity
cashregistersound
+ cbegin
cbgn
cbits
cbot
@@ -699,6 +700,7 @@
eachother
eaddrnotavail
easteregghunt
+ echofile
edcc
editcontroller
editgame
@@ -891,6 +893,7 @@
floofcls
floooff
floop
+ flushhhhh
flycheck
fmod
fname
@@ -1391,6 +1394,7 @@
listvalidconfigs
lival
llzma
+ lmap
lmerged
lmod
lmodfile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0e80f79..ee10f39a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-### 1.7.7 (build 20723, api 7, 2022-08-26)
+### 1.7.7 (build 20730, api 7, 2022-09-02)
- Added `ba.app.meta.load_exported_classes()` for loading classes discovered by the meta subsystem cleanly in a background thread.
- Improved logging of missing playlist game types.
- Some ba.Lstr functionality can now be used in background threads.
@@ -8,6 +8,7 @@
- Added support for the console tool in the new devices section on ballistica.net.
- Increased timeouts in net-testing gui and a few other places to be able to better diagnose/handle places with very poor connectivity.
- Removed `Platform::SetLastPyCall()` which was just for debugging and which has not been useful in a while.
+- Moved some app bootstrapping from the C++ layer to the ba._bootstrap module.
### 1.7.6 (build 20687, api 7, 2022-08-11)
- Cleaned up da MetaSubsystem code.
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index 306022cc..075dc768 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -17,6 +17,7 @@
"ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc",
+ "ba_data/python/ba/__pycache__/_bootstrap.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc",
"ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc",
@@ -82,6 +83,7 @@
"ba_data/python/ba/_assetmanager.py",
"ba_data/python/ba/_asyncio.py",
"ba_data/python/ba/_benchmark.py",
+ "ba_data/python/ba/_bootstrap.py",
"ba_data/python/ba/_campaign.py",
"ba_data/python/ba/_cloud.py",
"ba_data/python/ba/_collision.py",
@@ -511,6 +513,7 @@
"ba_data/python/efro/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/call.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc",
+ "ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/rpc.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/terminal.cpython-310.opt-1.pyc",
"ba_data/python/efro/__pycache__/util.cpython-310.opt-1.pyc",
@@ -532,6 +535,7 @@
"ba_data/python/efro/dataclassio/_prep.py",
"ba_data/python/efro/dataclassio/extras.py",
"ba_data/python/efro/error.py",
+ "ba_data/python/efro/log.py",
"ba_data/python/efro/message/__init__.py",
"ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc",
"ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc",
diff --git a/assets/Makefile b/assets/Makefile
index bda34d3f..88f68306 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -150,6 +150,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
build/ba_data/python/ba/_assetmanager.py \
build/ba_data/python/ba/_asyncio.py \
build/ba_data/python/ba/_benchmark.py \
+ build/ba_data/python/ba/_bootstrap.py \
build/ba_data/python/ba/_campaign.py \
build/ba_data/python/ba/_cloud.py \
build/ba_data/python/ba/_collision.py \
@@ -399,6 +400,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
build/ba_data/python/ba/__pycache__/_assetmanager.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_asyncio.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_benchmark.cpython-310.opt-1.pyc \
+ build/ba_data/python/ba/__pycache__/_bootstrap.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_campaign.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_cloud.cpython-310.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_collision.cpython-310.opt-1.pyc \
@@ -664,6 +666,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/_prep.py \
build/ba_data/python/efro/dataclassio/extras.py \
build/ba_data/python/efro/error.py \
+ build/ba_data/python/efro/log.py \
build/ba_data/python/efro/message/__init__.py \
build/ba_data/python/efro/message/_message.py \
build/ba_data/python/efro/message/_module.py \
@@ -694,6 +697,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
build/ba_data/python/efro/dataclassio/__pycache__/_prep.cpython-310.opt-1.pyc \
build/ba_data/python/efro/dataclassio/__pycache__/extras.cpython-310.opt-1.pyc \
build/ba_data/python/efro/__pycache__/error.cpython-310.opt-1.pyc \
+ build/ba_data/python/efro/__pycache__/log.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/__init__.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/_message.cpython-310.opt-1.pyc \
build/ba_data/python/efro/message/__pycache__/_module.cpython-310.opt-1.pyc \
diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py
new file mode 100644
index 00000000..cb12e496
--- /dev/null
+++ b/assets/src/ba_data/python/ba/_bootstrap.py
@@ -0,0 +1,185 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Bootstrapping."""
+from __future__ import annotations
+
+import threading
+from typing import TYPE_CHECKING
+
+import _ba
+
+if TYPE_CHECKING:
+ from typing import Any, TextIO, Callable
+
+
+def bootstrap() -> None:
+ """Run bootstrapping logic.
+
+ This is the very first userland code that runs.
+ It sets up low level environment bits and creates the app instance.
+ """
+ import os
+ import sys
+ import signal
+
+ # The first thing we set up is capturing/redirecting Python
+ # stdout/stderr so we can at least debug problems on systems where
+ # native stdout/stderr is not easily accessible (looking at you, Android).
+ sys.stdout = _Redirect(sys.stdout, _ba.print_stdout) # type: ignore
+ sys.stderr = _Redirect(sys.stderr, _ba.print_stderr) # type: ignore
+
+ env = _ba.env()
+
+ # Give a soft warning if we're being used with a different binary
+ # version than we expect.
+ expected_build = 20730
+ running_build: int = env['build_number']
+ if running_build != expected_build:
+ print(
+ f'WARNING: These script files are meant to be used with'
+ f' Ballistica build {expected_build}.\n'
+ f' You are running build {running_build}.'
+ f' This might cause the app to error or misbehave.',
+ file=sys.stderr)
+
+ # Tell Python to not handle SIGINT itself (it normally generates
+ # KeyboardInterrupts which make a mess; we want to intercept them
+ # for simple clean exit). We capture interrupts per-platform in
+ # the C++ layer.
+ # Note: I tried creating a handler in Python but it seemed to often have
+ # a delay of up to a second before getting called. (not a huge deal
+ # but I'm picky).
+ signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
+
+ # ..though it turns out we need to set up our C signal handling AFTER
+ # we've told Python to disable its own; otherwise (on Mac at least) it
+ # wipes out our existing C handler.
+ _ba.setup_sigint()
+
+ # Sanity check: we should always be run in UTF-8 mode.
+ if sys.flags.utf8_mode != 1:
+ print(
+ 'ERROR: Python\'s UTF-8 mode is not set.'
+ ' This will likely result in errors.',
+ file=sys.stderr)
+
+ debug_build = env['debug_build']
+
+ # We expect dev_mode on in debug builds and off otherwise.
+ if debug_build != sys.flags.dev_mode:
+ print(
+ f'WARNING: Mismatch in debug_build {debug_build}'
+ f' and sys.flags.dev_mode {sys.flags.dev_mode}',
+ file=sys.stderr)
+
+ # In embedded situations (when we're providing our own Python) let's
+ # also provide our own root certs so ssl works. We can consider overriding
+ # this in particular embedded cases if we can verify that system certs
+ # are working.
+ # (We also allow forcing this via an env var if the user desires)
+ if (_ba.contains_python_dist()
+ or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
+ import certifi
+
+ # Let both OpenSSL and requests (if present) know to use this.
+ os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
+ certifi.where())
+
+ # FIXME: I think we should init Python in the main thread, which should
+ # also avoid these issues. (and also might help us play better with
+ # Python debuggers?)
+
+ # Gloriously hacky workaround here:
+ # Our 'main' Python thread is the game thread (not the app's main
+ # thread) which means it has a small stack compared to the main
+ # thread (at least on apple). Sadly it turns out this causes the
+ # debug build of Python to blow its stack immediately when doing
+ # some big imports.
+ # Normally we'd just give the game thread the same stack size as
+ # the main thread and that'd be the end of it. However
+ # we're using std::threads which it turns out have no way to set
+ # the stack size (as of fall '19). Grumble.
+ #
+ # However python threads *can* take custom stack sizes.
+ # (and it appears they might use the main thread's by default?..)
+ # ...so as a workaround in the debug version, we can run problematic
+ # heavy imports here in another thread and all is well.
+ # If we ever see stack overflows in our release build we'll have
+ # to take more drastic measures like switching from std::threads
+ # to pthreads.
+
+ if debug_build:
+
+ # noinspection PyUnresolvedReferences
+ def _thread_func() -> None:
+ # pylint: disable=unused-import
+ import json
+ import urllib.request
+
+ testthread = threading.Thread(target=_thread_func)
+ testthread.start()
+ testthread.join()
+ del testthread
+
+ # Clear out the standard quit/exit messages since they don't work for us.
+ # pylint: disable=c-extension-no-member
+ if not TYPE_CHECKING:
+ import __main__
+ del __main__.__builtins__.quit
+ del __main__.__builtins__.exit
+
+ # Now spin up our App instance and store it on both _ba and ba.
+ from ba._app import App
+ import ba
+ _ba.app = ba.app = App()
+
+
+class _Redirect:
+
+ def __init__(self, original: TextIO, call: Callable[[str], None]) -> None:
+ self._lock = threading.Lock()
+ self._linebits: list[str] = []
+ self._original = original
+ self._call = call
+ self._pending_ship = False
+
+ def write(self, sval: Any) -> None:
+ """Override standard write call."""
+
+ self._call(sval)
+
+ # Now do logging:
+ # Add it to our accumulated line.
+ # If the message ends in a newline, we can ship it
+ # immediately as a log entry. Otherwise, schedule a ship
+ # next cycle (if it hasn't yet at that point) so that we
+ # can accumulate subsequent prints.
+ # (so stuff like print('foo', 123, 'bar') will ship as one entry)
+ with self._lock:
+ self._linebits.append(sval)
+ if sval.endswith('\n'):
+ self._shiplog()
+ else:
+ _ba.pushcall(self._shiplog,
+ from_other_thread=True,
+ suppress_other_thread_warning=True)
+
+ def _shiplog(self) -> None:
+ with self._lock:
+ line = ''.join(self._linebits)
+ if not line:
+ return
+ self._linebits = []
+
+ # Log messages aren't expected to have trailing newlines.
+ if line.endswith('\n'):
+ line = line[:-1]
+ _ba.log(line, to_stdout=False)
+
+ def flush(self) -> None:
+ """Flush the file."""
+ self._original.flush()
+
+ def isatty(self) -> bool:
+ """Are we a terminal?"""
+ return self._original.isatty()
diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py
index c499435f..263f8e3e 100644
--- a/assets/src/ba_data/python/bastd/game/football.py
+++ b/assets/src/ba_data/python/bastd/game/football.py
@@ -106,8 +106,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
],
default=1.0,
),
+ ba.BoolSetting('Epic Mode', default=False),
]
- default_music = ba.MusicType.FOOTBALL
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
@@ -143,6 +143,10 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
self._flag_respawn_light: ba.NodeActor | None = None
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
+ self._epic_mode = bool(settings['Epic Mode'])
+ self.slow_motion = self._epic_mode
+ self.default_music = (ba.MusicType.EPIC
+ if self._epic_mode else ba.MusicType.FOOTBALL)
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
@@ -330,7 +334,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS,
version='B')
- default_music = ba.MusicType.FOOTBALL
# FIXME: Need to update co-op games to use getscoreconfig.
def get_score_type(self) -> str:
diff --git a/assets/src/ba_data/python/bastd/game/hockey.py b/assets/src/ba_data/python/bastd/game/hockey.py
index 97213a60..11110e78 100644
--- a/assets/src/ba_data/python/bastd/game/hockey.py
+++ b/assets/src/ba_data/python/bastd/game/hockey.py
@@ -137,8 +137,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]):
],
default=1.0,
),
+ ba.BoolSetting('Epic Mode', default=False),
]
- default_music = ba.MusicType.HOCKEY
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
@@ -203,6 +203,10 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]):
self._puck: Puck | None = None
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
+ self._epic_mode = bool(settings['Epic Mode'])
+ self.slow_motion = self._epic_mode
+ self.default_music = (ba.MusicType.EPIC
+ if self._epic_mode else ba.MusicType.HOCKEY)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/assets/src/ba_data/python/bastd/game/keepaway.py
index 0dd355ee..3d3477e3 100644
--- a/assets/src/ba_data/python/bastd/game/keepaway.py
+++ b/assets/src/ba_data/python/bastd/game/keepaway.py
@@ -76,9 +76,9 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
],
default=1.0,
),
+ ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
- default_music = ba.MusicType.KEEP_AWAY
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
@@ -115,6 +115,10 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
self._flag: Flag | None = None
self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit'])
+ self._epic_mode = bool(settings['Epic Mode'])
+ self.slow_motion = self._epic_mode
+ self.default_music = (ba.MusicType.EPIC
+ if self._epic_mode else ba.MusicType.KEEP_AWAY)
def get_instance_description(self) -> str | Sequence:
return 'Carry the flag for ${ARG1} seconds.', self._hold_time
diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/assets/src/ba_data/python/bastd/game/kingofthehill.py
index becdc8b7..677c9fdf 100644
--- a/assets/src/ba_data/python/bastd/game/kingofthehill.py
+++ b/assets/src/ba_data/python/bastd/game/kingofthehill.py
@@ -79,6 +79,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
],
default=1.0,
),
+ ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@@ -115,6 +116,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
self._scoring_team: weakref.ref[Team] | None = None
self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit'])
+ self._epic_mode = bool(settings['Epic Mode'])
self._flag_region_material = ba.Material()
self._flag_region_material.add_actions(
conditions=('they_have_material', shared.player_material),
@@ -128,7 +130,9 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
))
# Base class overrides.
- self.default_music = ba.MusicType.SCARY
+ self.slow_motion = self._epic_mode
+ self.default_music = (ba.MusicType.EPIC
+ if self._epic_mode else ba.MusicType.SCARY)
def get_instance_description(self) -> str | Sequence:
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index b9d0ddbe..b10c4aa3 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -188,6 +188,7 @@
cancelbtn
capitan
cargs
+ cbegin
cbgn
cbresults
cbtnoffs
@@ -362,6 +363,7 @@
dxgi
dynamicdata
echidna
+ echofile
edef
effmult
efro
@@ -460,6 +462,7 @@
floooff
floop
flopsy
+ flushhhhh
fname
fnode
fnumc
@@ -703,6 +706,7 @@
linkstoryboards
listobj
llock
+ lmap
localmodlibs
localns
lockpath
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index e781430d..7ece08e1 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 modify them here.
-const int kAppBuildNumber = 20723;
+const int kAppBuildNumber = 20730;
const char* kAppVersion = "1.7.7";
// Our standalone globals.
diff --git a/src/meta/bameta/python_embedded/bootstrap.py b/src/meta/bameta/python_embedded/bootstrap.py
index 26e409ca..d84cd45d 100644
--- a/src/meta/bameta/python_embedded/bootstrap.py
+++ b/src/meta/bameta/python_embedded/bootstrap.py
@@ -1,78 +1,20 @@
# Released under the MIT License. See LICENSE for details.
+#
"""Initial ballistica bootstrapping."""
from __future__ import annotations
-import os
import sys
-import signal
-import threading
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
- from typing import Any, TextIO, Callable
+ pass
+# All we do here is make our script files accessible and then hand it off
+# to them.
-class _BAConsoleRedirect:
-
- def __init__(self, original: TextIO, call: Callable[[str], None]) -> None:
- self._lock = threading.Lock()
- self._linebits: list[str] = []
- self._original = original
- self._call = call
- self._pending_ship = False
-
- def write(self, sval: Any) -> None:
- """Override standard write call."""
-
- self._call(sval)
-
- # Now do logging:
- # Add it to our accumulated line.
- # If the message ends in a newline, we can ship it
- # immediately as a log entry. Otherwise, schedule a ship
- # next cycle (if it hasn't yet at that point) so that we
- # can accumulate subsequent prints.
- # (so stuff like print('foo', 123, 'bar') will ship as one entry)
- with self._lock:
- self._linebits.append(sval)
- if sval.endswith('\n'):
- self._shiplog()
- else:
- _ba.pushcall(self._shiplog,
- from_other_thread=True,
- suppress_other_thread_warning=True)
-
- def _shiplog(self) -> None:
- with self._lock:
- line = ''.join(self._linebits)
- if not line:
- return
- self._linebits = []
-
- # Log messages aren't expected to have trailing newlines.
- if line.endswith('\n'):
- line = line[:-1]
- _ba.log(line, to_stdout=False)
-
- def flush(self) -> None:
- """Flush the file."""
- self._original.flush()
-
- def isatty(self) -> bool:
- """Are we a terminal?"""
- return self._original.isatty()
-
-
-# The very first thing we set up is redirecting Python stdout/stderr so
-# we can at least debug problems on systems where native stdout/stderr
-# is not easily accessible (looking at you, Android).
-sys.stdout = _BAConsoleRedirect(sys.stdout, _ba.print_stdout) # type: ignore
-sys.stderr = _BAConsoleRedirect(sys.stderr, _ba.print_stderr) # type: ignore
-
-# Now get access to our various script files.
# Let's lookup mods first (so users can do whatever they want).
# and then our bundled scripts last (don't want bundled site-package
# stuff overwriting system versions)
@@ -80,93 +22,9 @@ sys.path.insert(0, _ba.env()['python_directory_user'])
sys.path.append(_ba.env()['python_directory_app'])
sys.path.append(_ba.env()['python_directory_app_site'])
-# Tell Python to not handle SIGINT itself (it normally generates
-# KeyboardInterrupts which make a mess; we want to intercept them
-# for simple clean exit). We capture interrupts per-platform in
-# the C++ layer.
-# Note: I tried creating a handler in Python but it seemed to often have
-# a delay of up to a second before getting called. (not a huge deal
-# but I'm picky).
-signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
-
-# ..though it turns out we need to set up our C signal handling AFTER
-# we've told Python to disable its own; otherwise (on Mac at least) it
-# wipes out our existing C handler.
-_ba.setup_sigint()
-
-# Sanity check: we should always be run in UTF-8 mode.
-if sys.flags.utf8_mode != 1:
- print('ERROR: Python\'s UTF-8 mode is not set.'
- ' This will likely result in errors.')
-
-debug_build = _ba.env()['debug_build']
-
-# We expect dev_mode on in debug builds and off otherwise.
-if debug_build != sys.flags.dev_mode:
- print(f'WARNING: Mismatch in debug_build {debug_build}'
- f' and sys.flags.dev_mode {sys.flags.dev_mode}')
-
-# In embedded situations (when we're providing our own Python) let's
-# also provide our own root certs so ssl works. We can consider overriding
-# this in particular embedded cases if we can verify that system certs
-# are working.
-# (We also allow forcing this via an env var if the user desires)
-# pylint: disable=wrong-import-position
-if (_ba.contains_python_dist()
- or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
- import certifi
-
- # Let both OpenSSL and requests (if present) know to use this.
- os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
- certifi.where())
-
-# FIXME: I think we should init Python in the main thread, which should
-# also avoid these issues. (and also might help us play better with
-# Python debuggers?)
-
-# Gloriously hacky workaround here:
-# Our 'main' Python thread is the game thread (not the app's main
-# thread) which means it has a small stack compared to the main
-# thread (at least on apple). Sadly it turns out this causes the
-# debug build of Python to blow its stack immediately when doing
-# some big imports.
-# Normally we'd just give the game thread the same stack size as
-# the main thread and that'd be the end of it. However
-# we're using std::threads which it turns out have no way to set
-# the stack size (as of fall '19). Grumble.
-#
-# However python threads *can* take custom stack sizes.
-# (and it appears they might use the main thread's by default?..)
-# ...so as a workaround in the debug version, we can run problematic
-# heavy imports here in another thread and all is well.
-# If we ever see stack overflows in our release build we'll have
-# to take more drastic measures like switching from std::threads
-# to pthreads.
-
-if debug_build:
-
- # noinspection PyUnresolvedReferences
- def _thread_func() -> None:
- # pylint: disable=unused-import
- import json
- import urllib.request
-
- testthread = threading.Thread(target=_thread_func)
- testthread.start()
- testthread.join()
- del testthread
-
-# Clear out the standard quit/exit messages since they don't work for us.
-# pylint: disable=c-extension-no-member
-if not TYPE_CHECKING:
- import __main__
- del __main__.__builtins__.quit
- del __main__.__builtins__.exit
-
-# Now spin up our App instance, store it on both _ba and ba,
-# and return it to the C++ layer.
+# The import is down here since it won't work until we muck with paths.
# noinspection PyProtectedMember
-from ba._app import App
-import ba
+# pylint: disable=wrong-import-position
+from ba._bootstrap import bootstrap
-_ba.app = ba.app = App()
+bootstrap()
diff --git a/tools/efro/log.py b/tools/efro/log.py
new file mode 100644
index 00000000..af647f56
--- /dev/null
+++ b/tools/efro/log.py
@@ -0,0 +1,247 @@
+# Released under the MIT License. See LICENSE for details.
+#
+"""Logging functionality."""
+from __future__ import annotations
+
+import sys
+import time
+import logging
+import datetime
+import threading
+from enum import Enum
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Annotated
+
+from efro.util import utc_now
+from efro.terminal import TerminalColor
+from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json
+
+if TYPE_CHECKING:
+ from pathlib import Path
+ from typing import Any, Callable
+
+
+class LogLevel(Enum):
+ """Severity level for a log entry.
+
+ These enums have numeric values so they can be compared in severity.
+ Note that these values are not currently interchangeable with the
+ logging.ERROR, logging.DEBUG, etc. values.
+ """
+ DEBUG = 0
+ INFO = 1
+ WARNING = 2
+ ERROR = 3
+ CRITICAL = 4
+
+
+@ioprepped
+@dataclass
+class LogEntry:
+ """Single logged message."""
+ name: Annotated[str,
+ IOAttrs('n', soft_default='root', store_default=False)]
+ message: Annotated[str, IOAttrs('m')]
+ level: Annotated[LogLevel, IOAttrs('l')]
+ time: Annotated[datetime.datetime, IOAttrs('t')]
+
+
+class LogHandler(logging.Handler):
+ """Fancy-pants handler for logging output.
+
+ Writes logs to disk in structured json format and echoes them
+ to stdout/stderr with pretty colors.
+ """
+
+ def __init__(self,
+ path: str | Path | None,
+ echofile: Any,
+ suppress_non_root_debug: bool = False):
+ super().__init__()
+ # pylint: disable=consider-using-with
+ self._file = (None
+ if path is None else open(path, 'w', encoding='utf-8'))
+ self._echofile = echofile
+ self._callbacks: list[Callable[[LogEntry], None]] = []
+ self._suppress_non_root_debug = suppress_non_root_debug
+
+ def emit(self, record: logging.LogRecord) -> None:
+
+ # Special case - filter out this common extra-chatty category.
+ # TODO - should use a standard logging.Filter for this.
+ if (self._suppress_non_root_debug and record.name != 'root'
+ and record.levelname == 'DEBUG'):
+ return
+
+ # Bake down all log formatting into a simple string.
+ msg = self.format(record)
+
+ # Translate Python log levels to our own.
+ level = {
+ 'DEBUG': LogLevel.DEBUG,
+ 'INFO': LogLevel.INFO,
+ 'WARNING': LogLevel.WARNING,
+ 'ERROR': LogLevel.ERROR,
+ 'CRITICAL': LogLevel.CRITICAL
+ }[record.levelname]
+
+ entry = LogEntry(message=msg,
+ name=record.name,
+ level=level,
+ time=datetime.datetime.fromtimestamp(
+ record.created, datetime.timezone.utc))
+
+ for call in self._callbacks:
+ call(entry)
+
+ # Also route log entries to the echo file (generally stdout/stderr)
+ # with pretty colors.
+ if self._echofile is not None:
+ cbegin: str
+ cend: str
+ cbegin, cend = {
+ LogLevel.DEBUG:
+ (TerminalColor.CYAN.value, TerminalColor.RESET.value),
+ LogLevel.INFO: ('', ''),
+ LogLevel.WARNING:
+ (TerminalColor.YELLOW.value, TerminalColor.RESET.value),
+ LogLevel.ERROR:
+ (TerminalColor.RED.value, TerminalColor.RESET.value),
+ LogLevel.CRITICAL:
+ (TerminalColor.STRONG_MAGENTA.value +
+ TerminalColor.BOLD.value + TerminalColor.BG_BLACK.value,
+ TerminalColor.RESET.value),
+ }[level]
+
+ self._echofile.write(f'{cbegin}{msg}{cend}\n')
+
+ # Note to self: it sounds like logging wraps calls to us
+ # in a lock so we shouldn't have to worry about garbled
+ # json output due to multiple threads writing at once,
+ # but may be good to find out for sure?
+ if self._file is not None:
+ entry_s = dataclass_to_json(entry)
+ assert '\n' not in entry_s # make sure its a single line
+ print(entry_s, file=self._file, flush=True)
+
+ def emit_custom(self, name: str, message: str, level: LogLevel) -> None:
+ """Custom emit call for our stdout/stderr redirection."""
+ entry = LogEntry(name=name,
+ message=message,
+ level=level,
+ time=utc_now())
+
+ for call in self._callbacks:
+ call(entry)
+
+ if self._file is not None:
+ entry_s = dataclass_to_json(entry)
+ assert '\n' not in entry_s # Make sure its a single line.
+ print(entry_s, file=self._file, flush=True)
+
+ def add_callback(self, call: Callable[[LogEntry], None]) -> None:
+ """Add a callback to be run for each added entry."""
+ self._callbacks.append(call)
+
+
+class LogRedirect:
+ """A file-like object for redirecting stdout/stderr to our log."""
+
+ def __init__(self, name: str, orig_out: Any, log_handler: LogHandler,
+ log_level: LogLevel):
+ self._name = name
+ self._orig_out = orig_out
+ self._log_handler = log_handler
+ self._log_level = log_level
+ self._chunk = ''
+ self._chunk_start_time = 0.0
+ self._lock = threading.Lock()
+
+ def write(self, s: str) -> None:
+ """Write something to output."""
+
+ assert isinstance(s, str)
+
+ # First, ship it off to the original destination.
+ self._orig_out.write(s)
+
+ # Now add this to our chunk and ship completed chunks
+ # off to the logger.
+ # Let's consider a chunk completed when we're passed
+ # a single '\n' by itself. (print() statement will do
+ # this at the end by default).
+ # We may get some false positives/negatives this way
+ # but it should result in *most* big multi-line print
+ # statements being wrapped into a single log entry.
+ # Also, flush with only_old=True can be called periodically
+ # to dump any pending chunks that don't happen to fit
+ # this pattern.
+ with self._lock:
+ if s == '\n':
+ self._log_handler.emit_custom(name=self._name,
+ message=self._chunk,
+ level=self._log_level)
+ self._chunk = ''
+ else:
+ if self._chunk == '':
+ self._chunk_start_time = time.time()
+ self._chunk += s
+
+ def flush(self, only_old: bool = False) -> None:
+ """Flushhhhh!"""
+ self._orig_out.flush()
+ if only_old and time.time() - self._chunk_start_time < 0.5:
+ return
+ with self._lock:
+ if self._chunk != '':
+ chunk = self._chunk
+ if chunk.endswith('\n'):
+ chunk = chunk[:-1]
+ self._log_handler.emit_custom(name=self._name,
+ message=chunk,
+ level=self._log_level)
+ self._chunk = ''
+
+
+def setup_logging(log_path: str | Path | None,
+ level: LogLevel,
+ suppress_non_root_debug: bool = False) -> LogHandler:
+ """Set up our logging environment.
+
+ Returns the custom handler which can be used to fetch information
+ about logs that have passed through it. (worst log-levels, etc.).
+ """
+
+ lmap = {
+ LogLevel.DEBUG: logging.DEBUG,
+ LogLevel.INFO: logging.INFO,
+ LogLevel.WARNING: logging.WARNING,
+ LogLevel.ERROR: logging.ERROR,
+ LogLevel.CRITICAL: logging.CRITICAL,
+ }
+
+ # Wire logger output to go to a structured log file.
+ # Also echo it to stderr IF we're running in a terminal.
+ loghandler = LogHandler(
+ log_path,
+ echofile=sys.stderr if sys.stderr.isatty() else None,
+ suppress_non_root_debug=suppress_non_root_debug)
+
+ logging.basicConfig(level=lmap[level],
+ format='%(message)s',
+ handlers=[loghandler])
+
+ # DISABLING THIS BIT FOR NOW - want to keep things as pure as possible.
+ if bool(False):
+ # Now wire Python stdout/stderr output to generate log entries
+ # in addition to its regular routing. Make sure to do this *after* we
+ # tell the log-handler to write to stderr, otherwise we get an infinite
+ # loop.
+ # NOTE: remember that this won't capture subcommands or other
+ # non-python stdout/stderr output.
+ sys.stdout = LogRedirect( # type: ignore
+ 'stdout', sys.stdout, loghandler, LogLevel.INFO)
+ sys.stderr = LogRedirect( # type: ignore
+ 'stderr', sys.stderr, loghandler, LogLevel.INFO)
+
+ return loghandler