diff --git a/.efrocachemap b/.efrocachemap
index 808538f8..19ae2423 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -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/8f/35/1a6fbc2bd9d367b5b5d8350199da",
+ "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/df/e7aae0645d3813227e32628a0ff3",
+ "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ce/43/1d18f5d73d3fe5d7f1ed4fdc472c",
+ "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cb/04/5dc6236fd0ebeafbd013299a4766",
+ "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/3a/bbe527aae553058d38a89a85e6b5",
+ "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/60/1c/2e11dded6067b1cb27e6f6d48a0a",
+ "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/fd/0f61ebccbcfb85d01693c431f5a6",
+ "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9b/94/8a16341d49d6de25102c07c70675",
+ "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ac/1d/d48d569a072d45d96ea86760b9f0",
+ "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d7/31/f3a671560f4efb8708430f0ce985",
+ "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6e/76/fa07d7183f1bd0d438657339d33a",
+ "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b0/8b/60a531e23f24bba638e3fc615ed3",
+ "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/1b/519b7ba8f1718787d8ab62f61222",
+ "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/a0/138ece248132798d69cc81bce581",
+ "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/58/18ba7845fb9524a5cfef4d14bf7f",
+ "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/93/7e04d239fd333188b1412272c873",
+ "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/44/cb/2144fb8e2fc054d605e8e3baea77",
+ "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/89/90/a98081fbe24f8d062443ce84f3cc",
+ "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c9/80/1de60807e22d9a46c6902badbe7f",
+ "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/d7/f1/e2d6d8fdedd4ec4f3a6c0cc6bc14",
"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/97/5d/5255b7a90235bc570e71bfaf9f60",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/07/0b/c65c6f6b009633a7cf66ea1c9e08",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/81/09/10f7873ec315479806a9daa6e100",
+ "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/09/ab4219ed9d6b6b63439a33f38aac",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/fd/ae/d0a4fb20969028322bab2ae2365d",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/0b/cf/4b7529302b842bc75695f93c4172",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/ad/b0/7d2ca14baad3fdb2aac296352570",
+ "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/68/f8/156cbf9f5cf0fcbae9f12e8b18e8",
"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"
}
\ 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..ca34f7b6 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 20725, api 7, 2022-09-01)
- 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.
diff --git a/assets/.asset_manifest_public.json b/assets/.asset_manifest_public.json
index 306022cc..b447e3ff 100644
--- a/assets/.asset_manifest_public.json
+++ b/assets/.asset_manifest_public.json
@@ -511,6 +511,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 +533,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..a8898ae9 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -664,6 +664,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 +695,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/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..1c7d2d9f 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 = 20725;
const char* kAppVersion = "1.7.7";
// Our standalone globals.
diff --git a/tools/efro/log.py b/tools/efro/log.py
new file mode 100644
index 00000000..a2e78637
--- /dev/null
+++ b/tools/efro/log.py
@@ -0,0 +1,241 @@
+# 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 typing import TYPE_CHECKING, Annotated
+from dataclasses import dataclass
+
+from efro.util import utc_now
+from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json
+from efro.terminal import TerminalColor
+
+if TYPE_CHECKING:
+ from typing import Any, Callable
+ from pathlib import Path
+
+
+class LogLevel(Enum):
+ """Severity level for a log entry.
+
+ Note: these are numeric values so they can be compared in severity.
+ """
+ DEBUG = 0
+ INFO = 1
+ WARNING = 2
+ ERROR = 3
+ CRITICAL = 4
+
+
+@ioprepped
+@dataclass
+class LogEntry:
+ """Structured log entry."""
+ 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 StructuredLogHandler(logging.StreamHandler):
+ """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:
+ if (self._suppress_non_root_debug and record.name != 'root'
+ and record.levelname == 'DEBUG'):
+ return
+ 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)
+ 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())
+
+ # Inform anyone who wants to know about this log's level.
+ 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: StructuredLogHandler, 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) -> StructuredLogHandler:
+ """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 = StructuredLogHandler(
+ 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