IQ.Pilot Release Commit @ 4fcea4d

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-20 11:06:57 -05:00
commit 7b20edda67
4602 changed files with 1122468 additions and 0 deletions
View File
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path
# NOTE: Do NOT import anything here that needs be built (e.g. params)
from openpilot.common.basedir import BASEDIR
from openpilot.common.spinner import Spinner
from openpilot.common.text_window import TextWindow
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.hardware import HARDWARE, AGNOS
from openpilot.system.version import get_build_metadata
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
TOTAL_SCONS_NODES = 5500
MAX_BUILD_PROGRESS = 100
def get_job_sequence() -> list[int]:
env_override = os.environ.get("SCONS_MAX_JOBS")
if env_override is not None:
try:
max_jobs = max(1, int(env_override))
except ValueError:
max_jobs = 1
else:
detected_jobs = os.cpu_count() or 2
max_jobs = min(detected_jobs, 3 if AGNOS else detected_jobs)
candidates = [max_jobs, max_jobs // 2, 1]
jobs: list[int] = []
for candidate in candidates:
candidate = max(1, int(candidate))
if candidate not in jobs:
jobs.append(candidate)
return jobs
def heal_submodules() -> None:
# An interrupted install can leave a submodule's worktree EMPTY while its
# git metadata (.git/modules/<name>, pinned HEAD) is fully present — git
# even reports it clean. The build then fails in confusing ways (missing
# tinygrad, missing compile3.py). Re-checkout is purely local: no network,
# no auth, just materializing the worktree from the objects already on disk.
gitmodules = Path(BASEDIR) / ".gitmodules"
if not gitmodules.is_file():
return # vendored/prebuilt tree, no submodules
try:
out = subprocess.check_output(["git", "config", "-f", str(gitmodules), "--get-regexp", r"submodule\..*\.path"],
cwd=BASEDIR, text=True)
except (subprocess.CalledProcessError, OSError):
return
for line in out.splitlines():
path = line.split(" ", 1)[1].strip()
p = Path(BASEDIR) / path
if p.is_dir() and not any(p.iterdir()):
cloudlog.warning(f"submodule {path} worktree is empty, re-checking out from local objects")
subprocess.run(["git", "submodule", "update", "--checkout", "--force", path], cwd=BASEDIR, check=False)
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
env = os.environ.copy()
env['SCONS_PROGRESS'] = "1"
heal_submodules()
extra_args = ["--minimal"] if minimal else []
if AGNOS:
HARDWARE.set_power_save(False)
os.sched_setaffinity(0, range(8)) # ensure we can use the isolcpus cores
# building with all cores can result in using too
# much memory, so retry with less parallelism
compile_output: list[bytes] = []
for n in get_job_sequence():
compile_output.clear()
scons: subprocess.Popen = subprocess.Popen(["scons", f"-j{int(n)}", "--cache-populate", *extra_args], cwd=BASEDIR, env=env, stderr=subprocess.PIPE)
assert scons.stderr is not None
# Read progress from stderr and update spinner
while scons.poll() is None:
try:
line = scons.stderr.readline()
if line is None:
continue
line = line.rstrip()
prefix = b'progress: '
if line.startswith(prefix):
i = int(line[len(prefix):])
spinner.update_progress(MAX_BUILD_PROGRESS * min(0.99, i / TOTAL_SCONS_NODES), 100.)
elif len(line):
compile_output.append(line)
print(line.decode('utf8', 'replace'))
except Exception:
pass
if scons.returncode == 0:
spinner.update_progress(100, 100.)
break
if scons.returncode != 0:
# Read remaining output
if scons.stderr is not None:
compile_output += scons.stderr.read().split(b'\n')
# Build failed log errors
error_s = b"\n".join(compile_output).decode('utf8', 'replace')
add_file_handler(cloudlog)
cloudlog.error("scons build failed\n" + error_s)
# Show TextWindow
spinner.close()
if not os.getenv("CI"):
with TextWindow("openpilot failed to build\n \n" + error_s) as t:
t.wait_for_exit()
exit(1)
# enforce max cache size
cache_files = [f for f in CACHE_DIR.rglob('*') if f.is_file()]
cache_files.sort(key=lambda f: f.stat().st_mtime)
cache_size = sum(f.stat().st_size for f in cache_files)
for f in cache_files:
if cache_size < MAX_CACHE_SIZE:
break
cache_size -= f.stat().st_size
f.unlink()
if __name__ == "__main__":
spinner = Spinner()
spinner.update_progress(0, 100)
build_metadata = get_build_metadata()
build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS)
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Define the service name
SERVICE_NAME="actions.runner.iqpilot.$(uname -n)"
# Function to control the service
control_service() {
local action=$1 # Store the function argument in a local variable
sudo systemctl $action ${SERVICE_NAME}
}
service_exists_and_is_loaded() {
sudo systemctl status ${SERVICE_NAME} &>/dev/null
if [[ $? -ne 4 ]]; then
return 0 # Service is known to systemd (i.e., loaded)
else
return 1 # Service is unknown to systemd (i.e., not loaded)
fi
}
# Check for required argument
if [[ -z $1 ]] || { [[ $1 != "start" ]] && [[ $1 != "stop" ]]; }; then
echo "Usage: $0 {start|stop}"
exit 1
fi
# Store the script argument in a descriptive variable
ACTION=$1
# Trap EXIT signal (Ctrl+C) and stop the service
trap 'control_service stop ; exit' SIGINT SIGKILL EXIT
# Enter the main loop
while true; do
# Check if the service is actually present on the system
if service_exists_and_is_loaded; then
control_service $ACTION # Call the function with the specified action
fi
sleep 1 # Pause before the next iteration
done
+153
View File
@@ -0,0 +1,153 @@
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! ------------------------------------------------------------
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
import datetime
import os
import signal
import sys
import time
import traceback
# sync $PWD before cereal/kj loads (and for spawned procs) or kj warns "PWD doesn't match"
os.environ['PWD'] = os.getcwd()
from cereal import log
import cereal.messaging as messaging
import openpilot.system.sentry as sentry
from openpilot.common.utils import atomic_write
from openpilot.common.params import Params, ParamKeyFlag
from openpilot.common.text_window import TextWindow
from openpilot.system.hardware import HARDWARE
from openpilot.system.loggerd.crash_recovery import recover_unclean_segments
from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog, heal_param_perms, migrate_renamed_params
from openpilot.system.manager.process import ensure_running
from openpilot.system.manager.process_config import managed_processes
from openpilot.iqpilot.konn3kt.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata
from openpilot.system.hardware.hw import Paths
def manager_init() -> None:
heal_param_perms()
save_bootlog()
# loggerd isn't running yet, so any leftover .lock marks an unclean shutdown:
# unlock those segments and preserve them (dashcam footage from power cuts)
try:
recover_unclean_segments()
except Exception:
cloudlog.exception("recover_unclean_segments failed")
build_metadata = get_build_metadata()
params = Params()
migrate_renamed_params(params) # REVERT ME! sunny->IQ param key migration (runs before defaults are filled)
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION)
params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION)
params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON)
if build_metadata.release_channel:
params.clear_all(ParamKeyFlag.DEVELOPMENT_ONLY)
# device boot mode
if params.get("DeviceBootMode") == 1: # start in Always Offroad mode
params.put_bool("OffroadMode", True)
if params.get_bool("RecordFrontLock"):
params.put_bool("RecordFront", True)
# set unset params to their default value
for k in params.all_keys():
default_value = params.get_default_value(k)
if default_value is not None and params.get(k) is None:
params.put(k, default_value)
try:
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import seed_default_bundle_if_unset
seed_default_bundle_if_unset(params)
except Exception:
cloudlog.exception("failed to seed default model bundle")
# Create folders needed for msgq
try:
os.mkdir(Paths.shm_path())
except FileExistsError:
pass
except PermissionError:
print(f"WARNING: failed to make {Paths.shm_path()}")
# set params
serial = HARDWARE.get_serial()
params.put("Version", build_metadata.openpilot.version)
params.put("GitCommit", build_metadata.openpilot.git_commit)
params.put("GitCommitDate", build_metadata.openpilot.git_commit_date)
params.put("GitBranch", build_metadata.channel)
params.put("GitRemote", build_metadata.openpilot.git_origin)
params.put_bool("IsDevelopmentBranch", build_metadata.development_channel)
params.put_bool("IsTestedBranch", build_metadata.tested_channel)
params.put_bool("IsReleaseBranch", build_metadata.release_channel)
params.put_bool("IsReleaseIqBranch", build_metadata.release_channel)
params.put("HardwareSerial", serial)
# set dongle id
reg_res = register(show_spinner=True)
if reg_res:
dongle_id = reg_res
else:
raise Exception(f"Registration failed for device {serial}")
os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog
os.environ['GIT_ORIGIN'] = build_metadata.openpilot.git_normalized_origin # Needed for swaglog
os.environ['GIT_BRANCH'] = build_metadata.channel # Needed for swaglog
os.environ['GIT_COMMIT'] = build_metadata.openpilot.git_commit # Needed for swaglog
if not build_metadata.openpilot.is_dirty:
os.environ['CLEAN'] = '1'
# init logging
sentry.init(sentry.SentryProject.SELFDRIVE)
cloudlog.bind_global(dongle_id=dongle_id,
version=build_metadata.openpilot.version,
origin=build_metadata.openpilot.git_normalized_origin,
branch=build_metadata.channel,
commit=build_metadata.openpilot.git_commit,
dirty=build_metadata.openpilot.is_dirty,
device=HARDWARE.get_device_type())
# preimport all processes
for p in managed_processes.values():
p.prepare()
def manager_cleanup() -> None:
# send signals to kill all procs
for p in managed_processes.values():
p.stop(block=False)
# ensure all are killed
for p in managed_processes.values():
p.stop(block=True)
cloudlog.info("everything is dead")
def manager_thread() -> None:
cloudlog.bind(daemon="manager")
cloudlog.info("manager start")
cloudlog.info({"environ": os.environ})
params = Params()
ignore: list[str] = []
if params.get("DongleId") in (None, UNREGISTERED_DONGLE_ID):
ignore += ["manage_hephaestusd", "iquploaderd"]
if os.getenv("NOBOARD") is not None:
ignore.append("pandad")
ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0]
sm = messaging.SubMaster(['deviceState', 'carParams', 'pandaStates'], poll='deviceState')
pm = messaging.PubMaster(['managerState'])
write_onroad_params(False, params)
ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore)
started_prev = False
ignition_prev = False
running_prev = None
while True:
sm.update(1000)
started = sm['deviceState'].started
if started and not started_prev:
params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION)
elif not started and started_prev:
params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION)
ignition = any(ps.ignitionLine or ps.ignitionCan for ps in sm['pandaStates'] if ps.pandaType != log.PandaState.PandaType.unknown)
if ignition and not ignition_prev:
params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON)
# update onroad params, which drives pandad's safety setter thread
if started != started_prev:
write_onroad_params(started, params)
started_prev = started
ignition_prev = ignition
ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)
# print only on change (reprinting every loop floods the shared tmux); always logged
procs = [p for p in managed_processes.values() if p.proc]
running = ' '.join(
("\u001b[32m{}\u001b[0m".format(p.name) if p.proc.is_alive()
else "\u001b[1;31m\u2717 {}\u001b[0m".format(p.name))
for p in procs)
cloudlog.debug(running)
alive = tuple(p.proc.is_alive() for p in procs)
if alive != running_prev:
print(running)
running_prev = alive
# send managerState
msg = messaging.new_message('managerState', valid=True)
msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()]
pm.send('managerState', msg)
# kick AGNOS power monitoring watchdog
try:
if sm.all_checks(['deviceState']):
with atomic_write("/var/tmp/power_watchdog", "w", overwrite=True) as f:
f.write(str(time.monotonic()))
except Exception:
pass
# Exit main loop when uninstall/shutdown/reboot is needed
shutdown = False
for param in ("DoUninstall", "DoShutdown", "DoReboot"):
if params.get_bool(param):
shutdown = True
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
cloudlog.warning(f"Shutting down manager - {param} set")
if shutdown:
break
def main() -> None:
manager_init()
if os.getenv("PREPAREONLY") is not None:
return
# SystemExit on sigterm
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(1))
try:
manager_thread()
except Exception:
traceback.print_exc()
sentry.capture_exception()
finally:
manager_cleanup()
params = Params()
if params.get_bool("DoUninstall"):
cloudlog.warning("uninstalling")
HARDWARE.uninstall()
elif params.get_bool("DoReboot"):
cloudlog.warning("reboot")
HARDWARE.reboot()
elif params.get_bool("DoShutdown"):
cloudlog.warning("shutdown")
HARDWARE.shutdown()
if __name__ == "__main__":
if not (os.getenv("SIMULATION") and sys.platform == "darwin"):
unblock_stdout()
try:
main()
except KeyboardInterrupt:
print("got CTRL-C, exiting")
except Exception:
add_file_handler(cloudlog)
cloudlog.exception("Manager failed to start")
try:
managed_processes['ui'].stop()
except Exception:
pass
# Show last 3 lines of traceback
error = traceback.format_exc(-3)
error = "Manager failed to start\n\n" + error
with TextWindow(error) as t:
t.wait_for_exit()
raise
# manual exit because we are forked
sys.exit(0)
+301
View File
@@ -0,0 +1,301 @@
import importlib
import os
import signal
import time
import subprocess
from pathlib import Path
from collections.abc import Callable, ValuesView
from abc import ABC, abstractmethod
from multiprocessing import Process
from setproctitle import setproctitle
from cereal import car, log
import cereal.messaging as messaging
import openpilot.system.sentry as sentry
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
try:
from openpilot.system.proprietary_runtime.runtime_paths import preferred_runner_path
except ModuleNotFoundError:
_VERIFIED_RUNNER_PATH = Path("/usr/libexec/iqpilot/iqpilot_bundle_runner")
_FALLBACK_RUNNER_PATH = Path("/data/openpilot/system/proprietary_runtime/iqpilot_bundle_runner")
def preferred_runner_path() -> Path:
if _VERIFIED_RUNNER_PATH.is_file() and os.access(_VERIFIED_RUNNER_PATH, os.X_OK):
return _VERIFIED_RUNNER_PATH
if os.getenv("IQPILOT_ALLOW_DEV_FALLBACKS") == "1" and _FALLBACK_RUNNER_PATH.is_file():
return _FALLBACK_RUNNER_PATH
return _VERIFIED_RUNNER_PATH
def launcher(proc: str, name: str) -> None:
try:
# import the process
mod = importlib.import_module(proc)
# rename the process
setproctitle(proc)
# create new context since we forked
messaging.reset_context()
# add daemon name tag to logs
cloudlog.bind(daemon=name)
sentry.set_tag("daemon", name)
# exec the process
mod.main()
except KeyboardInterrupt:
cloudlog.warning(f"child {proc} got SIGINT")
except Exception:
# can't install the crash handler because sys.excepthook doesn't play nice
# with threads, so catch it here.
sentry.capture_exception()
raise
def nativelauncher(pargs: list[str], cwd: str, name: str) -> None:
os.environ['MANAGER_DAEMON'] = name
# exec the process
os.chdir(cwd)
os.execvp(pargs[0], pargs)
def join_process(process: Process, timeout: float) -> None:
# Process().join(timeout) will hang due to a python 3 bug: https://bugs.python.org/issue28382
# We have to poll the exitcode instead
t = time.monotonic()
while time.monotonic() - t < timeout and process.exitcode is None:
time.sleep(0.001)
class ManagerProcess(ABC):
daemon = False
sigkill = False
should_run: Callable[[bool, Params, car.CarParams], bool]
proc: Process | None = None
enabled = True
name = ""
shutting_down = False
restart_if_crash = False
@abstractmethod
def prepare(self) -> None:
pass
@abstractmethod
def start(self) -> None:
pass
def restart(self) -> None:
self.stop(sig=signal.SIGKILL)
self.start()
def stop(self, retry: bool = True, block: bool = True, sig: signal.Signals | None = None) -> int | None:
if self.proc is None:
return None
if self.proc.exitcode is None:
if not self.shutting_down:
cloudlog.info(f"killing {self.name}")
if sig is None:
sig = signal.SIGKILL if self.sigkill else signal.SIGINT
self.signal(sig)
self.shutting_down = True
if not block:
return None
join_process(self.proc, 5)
# If process failed to die send SIGKILL
if self.proc.exitcode is None and retry:
cloudlog.info(f"killing {self.name} with SIGKILL")
self.signal(signal.SIGKILL)
self.proc.join()
ret = self.proc.exitcode
cloudlog.info(f"{self.name} is dead with {ret}")
if self.proc.exitcode is not None:
self.shutting_down = False
self.proc = None
return ret
def signal(self, sig: int) -> None:
if self.proc is None:
return
# Don't signal if already exited
if self.proc.exitcode is not None and self.proc.pid is not None:
return
# Can't signal if we don't have a pid
if self.proc.pid is None:
return
cloudlog.info(f"sending signal {sig} to {self.name}")
os.kill(self.proc.pid, sig)
def get_process_state_msg(self):
state = log.ManagerState.ProcessState.new_message()
state.name = self.name
if self.proc:
state.running = self.proc.is_alive()
state.shouldBeRunning = self.proc is not None and not self.shutting_down
state.pid = self.proc.pid or 0
state.exitCode = self.proc.exitcode or 0
return state
class NativeProcess(ManagerProcess):
def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False):
self.name = name
self.cwd = cwd
self.cmdline = cmdline
self.should_run = should_run
self.enabled = enabled
self.sigkill = sigkill
self.launcher = nativelauncher
def prepare(self) -> None:
pass
def start(self) -> None:
# In case we only tried a non blocking stop we need to stop it before restarting
if self.shutting_down:
self.stop()
if self.proc is not None:
return
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))
self.proc.start()
self.shutting_down = False
class BundleProcess(NativeProcess):
def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False):
self.bundle = bundle
self.entry = entry
self.restart_if_crash = restart_if_crash
runner_path = preferred_runner_path()
runner_cmd = str(runner_path) if runner_path.is_absolute() else "./iqpilot_bundle_runner"
runner_cwd = "" if runner_path.is_absolute() else "system/proprietary_runtime"
super().__init__(
name=name,
cwd=runner_cwd,
cmdline=[
runner_cmd,
"--bundle", bundle,
"--mode", "python-module",
"--entry", entry,
"--daemon-name", name,
],
should_run=should_run,
enabled=enabled,
sigkill=sigkill,
)
class PythonProcess(ManagerProcess):
def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False):
self.name = name
self.module = module
self.should_run = should_run
self.enabled = enabled
self.sigkill = sigkill
self.launcher = launcher
self.restart_if_crash = restart_if_crash
def prepare(self) -> None:
if self.enabled:
cloudlog.info(f"preimporting {self.module}")
importlib.import_module(self.module)
def start(self) -> None:
# In case we only tried a non blocking stop we need to stop it before restarting
if self.shutting_down:
self.stop()
if self.proc is not None:
return
cloudlog.info(f"starting python {self.module}")
self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name))
self.proc.start()
self.shutting_down = False
class DaemonProcess(ManagerProcess):
"""Python process that has to stay running across manager restart.
This is used for athena so you don't lose SSH access when restarting manager."""
def __init__(self, name, module, param_name, enabled=True):
self.name = name
self.module = module
self.param_name = param_name
self.enabled = enabled
self.params = None
@staticmethod
def should_run(started, params, CP):
return True
def prepare(self) -> None:
pass
def start(self) -> None:
if self.params is None:
self.params = Params()
pid = self.params.get(self.param_name)
if pid is not None:
try:
os.kill(int(pid), 0)
with open(f'/proc/{pid}/cmdline') as f:
if self.module in f.read():
# daemon is running
return
except (OSError, FileNotFoundError):
# process is dead
pass
cloudlog.info(f"starting daemon {self.name}")
proc = subprocess.Popen(['python', '-m', self.module],
stdin=open('/dev/null'),
stdout=open('/dev/null', 'w'),
stderr=open('/dev/null', 'w'),
preexec_fn=os.setpgrp)
self.params.put(self.param_name, proc.pid)
def stop(self, retry=True, block=True, sig=None) -> None:
pass
def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None, CP: car.CarParams=None,
not_run: list[str] | None=None) -> list[ManagerProcess]:
if not_run is None:
not_run = []
running = []
for p in procs:
if p.enabled and p.name not in not_run and p.should_run(started, params, CP):
if p.restart_if_crash and p.proc is not None and not p.proc.is_alive():
cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode})')
p.restart()
running.append(p)
else:
p.stop(block=False)
for p in running:
p.start()
return running
+211
View File
@@ -0,0 +1,211 @@
import os
import platform
from pathlib import Path
from cereal import car, custom
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE, PC, TICI
from openpilot.system.hardware.hw import Paths
from openpilot.system.manager.process import PythonProcess, NativeProcess, BundleProcess
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_model_runner
from iqpilot.konn3kt.service_health import hephaestus_ready
WEBCAM = os.getenv("USE_WEBCAM") is not None
def driverview(started: bool, params: Params, CP: car.CarParams) -> bool:
return started or params.get_bool("IsDriverViewEnabled")
def driver_monitoring(started: bool, params: Params, CP: car.CarParams) -> bool:
if os.path.exists('/tmp/lite_hw'):
return False
return driverview(started, params, CP)
def notcar(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and CP.notCar
def iscar(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not CP.notCar
def logging(started: bool, params: Params, CP: car.CarParams) -> bool:
run = (not CP.notCar) or not params.get_bool("DisableLogging")
return started and run and params.get_bool("DashcamEnabled")
def ublox_available() -> bool:
if HARDWARE.get_device_type() == "tizi" or os.path.exists('/tmp/lite_hw'):
return False
quectel_override = Path(Paths.persist_root()) / "comma" / "use-quectel-gps"
return os.path.exists('/dev/ttyHS0') and not quectel_override.exists()
def ublox(started: bool, params: Params, CP: car.CarParams) -> bool:
use_ublox = ublox_available()
if use_ublox != params.get_bool("UbloxAvailable"):
params.put_bool("UbloxAvailable", use_ublox)
return started and use_ublox
def joystick(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("JoystickDebugMode")
def not_joystick(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("JoystickDebugMode")
def long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("LongitudinalManeuverMode")
def not_long_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("LongitudinalManeuverMode")
def lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("LateralManeuverMode")
def not_lat_maneuver(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not params.get_bool("LateralManeuverMode")
def qcomgps(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not ublox_available()
def always_run(started: bool, params: Params, CP: car.CarParams) -> bool:
return True
def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started
def navd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("NavigationEnabled")
def navrenderd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("NavigationEnabled") and params.get_bool("OnScreenNavigation")
def iqmapd_needed(params: Params) -> bool:
return (
params.get_bool("RoadNameToggle")
or params.get_bool("ShowSpeedLimits")
or params.get_bool("SpeedLimitController")
or params.get_bool("EnableSpeedLimitControl")
or params.get_bool("EnableSpeedLimitPredicative")
or params.get_bool("MapCurveSpeedController")
or params.get_bool("VisionCurveSpeedController")
)
def iqmapd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("NavigationEnabled") and iqmapd_needed(params)
def mapd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and iqmapd_needed(params)
def constructiond_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("ConstructionZoneAssist")
def iqvd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("VisionVehicleTracks")
def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return not started
def livestream(started: bool, params: Params, CP: car.CarParams) -> bool:
# Konn3kt Live View: hephaestusd sets IsLiveStreaming when a viewer connects, so the
# manager brings up the stream encoder (and camerad/webrtcd when offroad) and tears them
# down cleanly when the session ends — no subprocess management inside hephaestusd.
return params.get_bool("IsLiveStreaming")
def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad)
def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool:
return hephaestus_ready(params)
def not_low_power(started: bool, params: Params, CP: car.CarParams) -> bool:
# FastSleep deep standby: heavy processes are shed offroad while DevicePowerState is low_power
return started or params.get("DevicePowerState") != "low_power"
def iquploaderd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
if not params.get_bool("OnroadUploads"):
return only_offroad(started, params, CP)
return always_run(started, params, CP)
def or_(*fns):
return lambda *args: any(fn(*args) for fn in fns)
def and_(*fns):
return lambda *args: all(fn(*args) for fn in fns)
procs = [
NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging),
NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad),
NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(notcar, livestream)),
PythonProcess("logmessaged", "system.logmessaged", always_run, restart_if_crash=True),
NativeProcess("camerad", "system/camerad", ["./camerad"], or_(driverview, livestream), enabled=not WEBCAM),
PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM),
PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"),
PythonProcess("journald", "system.journald", only_onroad, platform.system() != "Darwin"),
PythonProcess("micd", "system.micd", or_(iscar, livestream)),
PythonProcess("timed", "system.timed", always_run, enabled=not PC),
PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driver_monitoring, enabled=(WEBCAM or not PC)),
PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC),
PythonProcess("ui", "selfdrive.ui.ui", not_low_power, restart_if_crash=True),
PythonProcess("soundd", "selfdrive.ui.soundd", driverview),
PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad),
NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False),
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad),
PythonProcess("controlsd", "selfdrive.controls.controlsd", and_(not_joystick, iscar)),
PythonProcess("joystickd", "tools.joystick.joystickd", or_(joystick, notcar)),
PythonProcess("selfdrived", "selfdrive.selfdrived.selfdrived", only_onroad),
PythonProcess("card", "selfdrive.car.card", only_onroad),
PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driver_monitoring, enabled=(WEBCAM or not PC)),
PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI),
PythonProcess("pandad", "selfdrive.pandad.pandad", always_run),
PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad),
PythonProcess("lagd", "selfdrive.locationd.lagd", only_onroad),
PythonProcess("ubloxd", "system.ubloxd.ubloxd", ublox, enabled=TICI),
PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI),
PythonProcess("plannerd", "selfdrive.controls.plannerd", not_long_maneuver),
PythonProcess("maneuversd", "tools.longitudinal_maneuvers.maneuversd", long_maneuver),
PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver),
PythonProcess("radard", "selfdrive.controls.radard", only_onroad),
PythonProcess("hardwared", "system.hardware.hardwared", always_run, restart_if_crash=True),
PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC),
PythonProcess("updated", "system.updated.updated", and_(only_offroad, not_low_power), enabled=not PC),
BundleProcess("iquploaderd", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.uploaderd.iquploaderd", and_(iquploaderd_ready, not_low_power), restart_if_crash=True),
PythonProcess("statsd", "system.statsd", always_run),
PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", and_(only_onroad, not_lat_maneuver)),
# debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(iscar, livestream)),
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
]
# iqpilot
procs += [
# Models
PythonProcess("models_manager", "iqpilot.selfdrive.iqmodeld.models.manager", and_(only_offroad, not_low_power)),
NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model)),
BundleProcess("backup_manager_k3", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.backups.backup_orchestrator", and_(only_offroad, hephaestus_ready_shim, not_low_power)),
BundleProcess("navd", "iqpilot_navd_private", "iqpilot_private.navd.navd", navd_onroad, restart_if_crash=True),
BundleProcess("navrenderd", "iqpilot_navd_private", "iqpilot_private.navd.navrenderd", navrenderd_onroad, restart_if_crash=True),
BundleProcess("iqmapd", "iqpilot_navd_private", "iqpilot_private.navd.iqmapd", iqmapd_onroad, restart_if_crash=True),
# work-zone detector for Speed Limit Assist
PythonProcess("constructiond", "iqpilot.selfdrive.constructiond", constructiond_onroad, restart_if_crash=True),
# iqvd: vision vehicle detector for UI ambient track dots
BundleProcess("iqvd", "iqpilot_iqvd_private", "iqpilot_private.iqvd.iqvd", iqvd_onroad, restart_if_crash=True),
# mapd
NativeProcess("mapd", "third_party/mapd_pfeiferj", ["./mapd"], mapd_onroad),
PythonProcess("mapd_manager", "iqpilot.iq_maps.orchestrator", and_(only_offroad, not_low_power)),
# locationd
NativeProcess("iqlocd", "iqpilot/selfdrive/iqlocd", ["./iqlocd"], only_onroad),
]
managed_processes = {p.name: p for p in procs}
View File
+87
View File
@@ -0,0 +1,87 @@
import os
import pytest
import signal
import time
from cereal import car
from openpilot.common.params import Params
import openpilot.system.manager.manager as manager
from openpilot.system.manager.process import PythonProcess, ensure_running
from openpilot.system.manager.process_config import managed_processes, procs
from openpilot.system.hardware import HARDWARE
os.environ['FAKEUPLOAD'] = "1"
MAX_STARTUP_TIME = 3
BLACKLIST_PROCS = ['manage_hephaestusd', 'pandad', 'pigeond']
class TestManager:
def setup_method(self):
HARDWARE.set_power_save(False)
# ensure clean CarParams
params = Params()
params.clear_all()
def teardown_method(self):
manager.manager_cleanup()
def test_manager_prepare(self):
os.environ['PREPAREONLY'] = '1'
manager.main()
def test_duplicate_procs(self):
assert len(procs) == len(managed_processes), "Duplicate process names"
def test_models_manager_uses_public_wrapper(self):
proc = managed_processes["models_manager"]
assert isinstance(proc, PythonProcess)
assert proc.module == "iqpilot.selfdrive.iqmodeld.models.manager"
def test_blacklisted_procs(self):
# TODO: ensure there are blacklisted procs until we have a dedicated test
assert len(BLACKLIST_PROCS), "No blacklisted procs to test not_run"
def test_set_params_with_default_value(self):
params = Params()
params.clear_all()
os.environ['PREPAREONLY'] = '1'
manager.main()
for k in params.all_keys():
default_value = params.get_default_value(k)
if default_value is not None:
assert params.get(k) == default_value
assert params.get("OpenpilotEnabledToggle")
assert params.get("RouteCount") == 0
@pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
def test_clean_exit(self, subtests):
"""
Ensure all processes exit cleanly when stopped.
"""
HARDWARE.set_power_save(False)
manager.manager_init()
CP = car.CarParams.new_message()
procs = ensure_running(managed_processes.values(), True, Params(), CP, not_run=BLACKLIST_PROCS)
time.sleep(10)
for p in procs:
with subtests.test(proc=p.name):
state = p.get_process_state_msg()
assert state.running, f"{p.name} not running"
exit_code = p.stop(retry=False)
assert p.name not in BLACKLIST_PROCS, f"{p.name} was started"
assert exit_code is not None, f"{p.name} failed to exit"
# TODO: interrupted blocking read exits with 1 in cereal. use a more unique return code
exit_codes = [0, 1]
if p.sigkill:
exit_codes = [-signal.SIGKILL]
assert exit_code in exit_codes, f"{p.name} died with {exit_code}"