import errno import fcntl import os import sys import pathlib import shutil import signal import subprocess import tempfile import threading from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog def unblock_stdout() -> None: # get a non-blocking stdout child_pid, child_pty = os.forkpty() if child_pid != 0: # parent # child is in its own process group, manually pass kill signals signal.signal(signal.SIGINT, lambda signum, frame: os.kill(child_pid, signal.SIGINT)) signal.signal(signal.SIGTERM, lambda signum, frame: os.kill(child_pid, signal.SIGTERM)) fcntl.fcntl(sys.stdout, fcntl.F_SETFL, fcntl.fcntl(sys.stdout, fcntl.F_GETFL) | os.O_NONBLOCK) while True: try: dat = os.read(child_pty, 4096) except OSError as e: if e.errno == errno.EIO: break continue if not dat: break try: sys.stdout.write(dat.decode('utf8')) except (OSError, UnicodeDecodeError): pass # os.wait() returns a tuple with the pid and a 16 bit value # whose low byte is the signal number and whose high byte is the exit status exit_status = os.wait()[1] >> 8 os._exit(exit_status) def write_onroad_params(started, params): params.put_bool("IsOnroad", started) params.put_bool("IsOffroad", not started) def heal_param_perms(): """Self-heal for a boot-bricking failure mode: a stray root process occasionally writes a param (seen with RouteCount/CurrentRoute) as root:root 0600, which manager (comma) then can't read — crashing save_bootlog and any param read. Detect params we don't own and chown them back + make them readable. Needs root to chown another user's file, so it shells to passwordless sudo (device grants it); best-effort, never raises, never blocks boot. No-op when everything is already ours (the common case: no sudo).""" try: param_path = Params().get_param_path() uid, gid = os.getuid(), os.getgid() stray = [] for name in os.listdir(param_path): p = os.path.join(param_path, name) try: if os.stat(p).st_uid != uid: stray.append(p) except OSError: pass if stray: subprocess.run(["sudo", "-n", "chown", f"{uid}:{gid}", *stray], check=False, timeout=15, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.run(["sudo", "-n", "chmod", "644", *stray], check=False, timeout=15, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception: pass def save_bootlog(): # copy current params tmp = tempfile.mkdtemp() params_dirname = pathlib.Path(Params().get_param_path()).name params_dir = os.path.join(tmp, params_dirname) # Params are rewritten atomically (unlink + rename) by other processes, so a # value can vanish between copytree's listing and the copy; a param may also be # unreadable (e.g. a root-owned RouteCount/CurrentRoute). Skip any file we can't # copy instead of raising — the bootlog snapshot is best-effort and must NOT block boot. def _copy_skip_missing(src, dst, *, follow_symlinks=True): try: shutil.copy2(src, dst, follow_symlinks=follow_symlinks) except OSError: pass shutil.copytree(Params().get_param_path(), params_dir, dirs_exist_ok=True, copy_function=_copy_skip_missing) def fn(tmpdir): env = os.environ.copy() env['PARAMS_COPY_PATH'] = tmpdir subprocess.call("./bootlog", cwd=os.path.join(BASEDIR, "system/loggerd"), env=env) shutil.rmtree(tmpdir) t = threading.Thread(target=fn, args=(tmp, )) t.daemon = True t.start() # REVERT ME! --------------------------------------------------------------- # One-shot migration carrying values across the sunny -> IQ param key renames. # Old keys are no longer registered in params_keys.h, so their values are read # straight off disk, copied to the new IQ key, and the stale file removed. # Delete this block (and its manager_init() call) once fielded devices have # booted past it at least once. _RENAMED_PARAMS = { "QuietMode": "IQAlertSilence", "SpeedLimitMode": "IQSpeedAssistMode", "SpeedLimitPolicy": "IQSpeedAssistPolicy", "SpeedLimitOffsetType": "IQSpeedAssistOffsetType", "SpeedLimitValueOffset": "IQSpeedAssistValueOffset", "LaneTurnDesire": "IQLaneTurnDesire", "LaneTurnValue": "IQLaneTurnValue", "BlinkerPauseLateralControl": "IQBlinkerPauseLateral", "BlinkerMinLateralControlSpeed": "IQBlinkerMinLateralSpeed", "DevUIInfo": "IQDevUIInfo", "OffroadMode": "IQAlwaysOffroad", "AutoLaneChangeTimer": "IQLaneChangeTimer", "AutoLaneChangeBsmDelay": "IQLaneChangeBsmDelay", "LagdToggle": "IQLiveSteerDelay", "LagdToggleDelay": "IQSoftwareSteerDelay", "LagdValueCache": "IQSteerDelayCache", } def migrate_renamed_params(params: Params | None = None) -> None: """REVERT ME! carry stored values across the sunny->IQ key renames, then drop the old files. Copies at the file level: on-disk param values are raw bytes, so this preserves the exact stored representation and sidesteps put()'s typed-value check for BOOL/INT/FLOAT keys. """ p = params if params is not None else Params() for old, new in _RENAMED_PARAMS.items(): try: old_path = p.get_param_path(old) if not os.path.isfile(old_path): continue new_path = p.get_param_path(new) if not os.path.exists(new_path): with open(old_path, "rb") as f: value = f.read() tmp = new_path + ".tmp" with open(tmp, "wb") as f: f.write(value) f.flush() os.fsync(f.fileno()) os.rename(tmp, new_path) os.remove(old_path) except Exception: cloudlog.exception(f"param rename migration failed for {old} -> {new}") # END REVERT ME! ------------------------------------------------------------