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:
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -54,6 +55,14 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
|
||||
|
||||
if scons.returncode == 0:
|
||||
Path('/data/openpilot/prebuilt').touch()
|
||||
|
||||
# CLEARPILOT: update prebuilt spinner if the new build is newer
|
||||
new_spinner = Path(BASEDIR) / "selfdrive/ui/_spinner"
|
||||
old_spinner = Path(BASEDIR) / "selfdrive/ui/qt/spinner"
|
||||
if new_spinner.exists() and (not old_spinner.exists() or new_spinner.stat().st_mtime > old_spinner.stat().st_mtime):
|
||||
import shutil
|
||||
shutil.copy2(str(new_spinner), str(old_spinner))
|
||||
|
||||
break
|
||||
|
||||
if scons.returncode != 0:
|
||||
@@ -69,8 +78,13 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
|
||||
# Show TextWindow
|
||||
spinner.close()
|
||||
if not os.getenv("CI"):
|
||||
with TextWindow("openpilot failed to build\n \n" + error_s) as t:
|
||||
# CLEARPILOT: BUILD_ONLY mode shows error on screen but doesn't block
|
||||
t = TextWindow("openpilot failed to build\n \n" + error_s)
|
||||
if os.getenv("BUILD_ONLY"):
|
||||
print(error_s, file=sys.stderr)
|
||||
else:
|
||||
t.wait_for_exit()
|
||||
t.close()
|
||||
exit(1)
|
||||
|
||||
# enforce max cache size
|
||||
|
||||
@@ -16,7 +16,7 @@ from openpilot.common.text_window import TextWindow
|
||||
from openpilot.common.time import system_time_valid
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
|
||||
from openpilot.selfdrive.manager.process import ensure_running
|
||||
from openpilot.selfdrive.manager.process import ensure_running, update_log_dir_timestamp, session_log
|
||||
from openpilot.selfdrive.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
|
||||
from openpilot.common.swaglog import cloudlog, add_file_handler
|
||||
@@ -51,7 +51,26 @@ def frogpilot_boot_functions(frogpilot_functions):
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
def cleanup_old_logs(max_age_days=30):
|
||||
"""CLEARPILOT: delete session log directories older than max_age_days."""
|
||||
import shutil
|
||||
log_base = "/data/log2"
|
||||
if not os.path.exists(log_base):
|
||||
return
|
||||
cutoff = time.time() - (max_age_days * 86400)
|
||||
for entry in os.listdir(log_base):
|
||||
path = os.path.join(log_base, entry)
|
||||
if os.path.isdir(path):
|
||||
if os.path.getmtime(path) < cutoff:
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def manager_init(frogpilot_functions) -> None:
|
||||
cleanup_old_logs()
|
||||
|
||||
frogpilot_boot = threading.Thread(target=frogpilot_boot_functions, args=(frogpilot_functions,))
|
||||
frogpilot_boot.start()
|
||||
|
||||
@@ -367,6 +386,7 @@ def manager_thread(frogpilot_functions) -> None:
|
||||
cloudlog.bind(daemon="manager")
|
||||
cloudlog.info("manager start")
|
||||
cloudlog.info({"environ": os.environ})
|
||||
session_log.info("manager starting")
|
||||
|
||||
params = Params()
|
||||
params_memory = Params("/dev/shm/params")
|
||||
@@ -397,6 +417,7 @@ def manager_thread(frogpilot_functions) -> None:
|
||||
|
||||
if started and not started_prev:
|
||||
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
|
||||
session_log.info("onroad transition")
|
||||
|
||||
if openpilot_crashed:
|
||||
os.remove(os.path.join(sentry.CRASHES_DIR, 'error.txt'))
|
||||
@@ -404,6 +425,7 @@ def manager_thread(frogpilot_functions) -> None:
|
||||
elif not started and started_prev:
|
||||
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
|
||||
params_memory.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
|
||||
session_log.info("offroad transition")
|
||||
|
||||
# update onroad params, which drives boardd's safety setter thread
|
||||
if started != started_prev:
|
||||
@@ -413,6 +435,9 @@ def manager_thread(frogpilot_functions) -> None:
|
||||
|
||||
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
|
||||
|
||||
# CLEARPILOT: rename log directory once system time is valid
|
||||
update_log_dir_timestamp()
|
||||
|
||||
running = ' '.join("{}{}\u001b[0m".format("\u001b[32m" if p.proc.is_alive() else "\u001b[31m", p.name)
|
||||
for p in managed_processes.values() if p.proc)
|
||||
print(running)
|
||||
@@ -430,6 +455,7 @@ def manager_thread(frogpilot_functions) -> None:
|
||||
shutdown = True
|
||||
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
|
||||
cloudlog.warning(f"Shutting down manager - {param} set")
|
||||
session_log.info("manager shutting down: %s", param)
|
||||
|
||||
if shutdown:
|
||||
break
|
||||
@@ -481,6 +507,7 @@ if __name__ == "__main__":
|
||||
except Exception:
|
||||
add_file_handler(cloudlog)
|
||||
cloudlog.exception("Manager failed to start")
|
||||
session_log.critical("manager failed to start: %s", traceback.format_exc())
|
||||
|
||||
try:
|
||||
managed_processes['ui'].stop()
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user