session logging, build-only mode, park splash, boot logo spinner

- Per-process stderr logging to /data/log2/{session}/ with time-safe
  directory naming (boot-xxx renamed once GPS/NTP sets clock)
- Aggregate session.log with major events (start/stop, transitions)
- 30-day log rotation cleanup on manager startup
- build_only.sh: compile without starting manager, non-blocking error
  display, kills stale processes
- build.py: copies updated spinner binary after successful build
- Onroad park mode: show splash screen when car is on but in park,
  switch to camera view when shifted to drive, honor screen on/off
- Spinner: full-screen boot logo background from /usr/comma/bg.jpg
  with white progress bar overlay
- Boot logo: auto-regenerate when boot_logo.png changes, 140% scale
- launch_openpilot.sh: cd to /data/openpilot for reliable execution
- launch_chffrplus.sh: kill stale text error displays on startup
- spinner shell wrapper: prefer freshly built _spinner over prebuilt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 23:19:07 +00:00
parent 4f3bfb4e8c
commit 578277ab9c
16 changed files with 266 additions and 77 deletions

View File

@@ -2,6 +2,7 @@ import importlib
import os
import signal
import struct
import sys
import datetime
import time
import subprocess
@@ -17,17 +18,62 @@ import openpilot.selfdrive.sentry as sentry
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.time import system_time_valid
WATCHDOG_FN = "/dev/shm/wd_"
ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None
timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
_log_dir = f"/data/log2/{timestamp}"
# CLEARPILOT: time-safe log directory — use temporary name if clock is invalid (1970),
# rename to real timestamp once GPS/NTP resolves the time
if system_time_valid():
_log_dir = f"/data/log2/{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
_time_resolved = True
else:
_log_dir = f"/data/log2/boot-{int(time.monotonic())}"
_time_resolved = False
os.makedirs(_log_dir, exist_ok=True)
# CLEARPILOT: aggregate session log for major events
import logging
session_log = logging.getLogger("clearpilot.session")
session_log.setLevel(logging.DEBUG)
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
session_log.addHandler(_session_handler)
def launcher(proc: str, name: str) -> None:
def update_log_dir_timestamp():
"""Rename boot-xxx log dir to real timestamp once system time is valid."""
global _log_dir, _time_resolved, _session_handler
if _time_resolved:
return
if not system_time_valid():
return
new_dir = f"/data/log2/{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
try:
os.rename(_log_dir, new_dir)
_log_dir = new_dir
_time_resolved = True
# Re-point session log handler to renamed directory
session_log.removeHandler(_session_handler)
_session_handler.close()
_session_handler = logging.FileHandler(os.path.join(_log_dir, "session.log"))
_session_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
session_log.addHandler(_session_handler)
session_log.info("log directory renamed to %s", _log_dir)
except OSError:
pass
def launcher(proc: str, name: str, log_path: str) -> None:
# CLEARPILOT: redirect stderr to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
except Exception:
pass
try:
# import the process
mod = importlib.import_module(proc)
@@ -53,9 +99,16 @@ def launcher(proc: str, name: str) -> None:
raise
def nativelauncher(pargs: list[str], cwd: str, name: str) -> None:
def nativelauncher(pargs: list[str], cwd: str, name: str, log_path: str) -> None:
os.environ['MANAGER_DAEMON'] = name
# CLEARPILOT: redirect stderr to per-process log file
try:
log_file = open(log_path, 'a')
os.dup2(log_file.fileno(), sys.stderr.fileno())
except Exception:
pass
# exec the process
os.chdir(cwd)
os.execvp(pargs[0], pargs)
@@ -110,6 +163,7 @@ class ManagerProcess(ABC):
if dt > self.watchdog_max_dt:
if (self.watchdog_seen or self.always_watchdog and self.proc.exitcode is not None) and ENABLE_WATCHDOG:
cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})")
session_log.warning("watchdog timeout for %s (exitcode %s), restarting", self.name, self.proc.exitcode)
self.restart()
else:
self.watchdog_seen = True
@@ -139,6 +193,10 @@ class ManagerProcess(ABC):
ret = self.proc.exitcode
cloudlog.info(f"{self.name} is dead with {ret}")
if ret is not None and ret != 0:
session_log.error("process %s died with exit code %s", self.name, ret)
elif ret == 0:
session_log.info("process %s stopped (exit 0)", self.name)
if self.proc.exitcode is not None:
self.shutting_down = False
@@ -200,7 +258,8 @@ class NativeProcess(ManagerProcess):
cwd = os.path.join(BASEDIR, self.cwd)
cloudlog.info(f"starting process {self.name}")
self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name))
session_log.info("starting %s", self.name)
self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, log_path))
self.proc.start()
self.watchdog_seen = False
self.shutting_down = False
@@ -232,7 +291,8 @@ class PythonProcess(ManagerProcess):
global _log_dir
log_path = _log_dir+"/"+self.name+".log"
cloudlog.info(f"starting python {self.module}")
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name))
session_log.info("starting %s", self.name)
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, log_path))
self.proc.start()
self.watchdog_seen = False
self.shutting_down = False
@@ -276,6 +336,7 @@ class DaemonProcess(ManagerProcess):
pass
cloudlog.info(f"starting daemon {self.name}")
session_log.info("starting daemon %s", self.name)
proc = subprocess.Popen(['python', '-m', self.module],
stdin=open('/dev/null'),
stdout=open(log_path, 'a'),