mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-07-22 12:02:09 +08:00
154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
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",
|
|
}
|
|
|
|
|
|
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! ------------------------------------------------------------
|