|  |  |  | #!/usr/bin/env python3
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Safe Update: A simple service that waits for network access and tries to
 | 
					
						
							|  |  |  | # update every 10 minutes. It's intended to make the OP update process more
 | 
					
						
							|  |  |  | # robust against Git repository corruption. This service DOES NOT try to fix
 | 
					
						
							|  |  |  | # an already-corrupt BASEDIR Git repo, only prevent it from happening.
 | 
					
						
							|  |  |  | #
 | 
					
						
							|  |  |  | # During normal operation, both onroad and offroad, the update process makes
 | 
					
						
							|  |  |  | # no changes to the BASEDIR install of OP. All update attempts are performed
 | 
					
						
							|  |  |  | # in a disposable staging area provided by OverlayFS. It assumes the deleter
 | 
					
						
							|  |  |  | # process provides enough disk space to carry out the process.
 | 
					
						
							|  |  |  | #
 | 
					
						
							|  |  |  | # If an update succeeds, a flag is set, and the update is swapped in at the
 | 
					
						
							|  |  |  | # next reboot. If an update is interrupted or otherwise fails, the OverlayFS
 | 
					
						
							|  |  |  | # upper layer and metadata can be discarded before trying again.
 | 
					
						
							|  |  |  | #
 | 
					
						
							|  |  |  | # The swap on boot is triggered by launch_chffrplus.sh
 | 
					
						
							|  |  |  | # gated on the existence of $FINALIZED/.overlay_consistent and also the
 | 
					
						
							|  |  |  | # existence and mtime of $BASEDIR/.overlay_init.
 | 
					
						
							|  |  |  | #
 | 
					
						
							|  |  |  | # Other than build byproducts, BASEDIR should not be modified while this
 | 
					
						
							|  |  |  | # service is running. Developers modifying code directly in BASEDIR should
 | 
					
						
							|  |  |  | # disable this service.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os
 | 
					
						
							|  |  |  | import datetime
 | 
					
						
							|  |  |  | import subprocess
 | 
					
						
							|  |  |  | import psutil
 | 
					
						
							|  |  |  | import shutil
 | 
					
						
							|  |  |  | import signal
 | 
					
						
							|  |  |  | import fcntl
 | 
					
						
							|  |  |  | import time
 | 
					
						
							|  |  |  | import threading
 | 
					
						
							|  |  |  | from pathlib import Path
 | 
					
						
							|  |  |  | from typing import List, Tuple, Optional
 | 
					
						
							|  |  |  | from markdown_it import MarkdownIt
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from common.basedir import BASEDIR
 | 
					
						
							|  |  |  | from common.params import Params
 | 
					
						
							|  |  |  | from system.hardware import AGNOS, HARDWARE
 | 
					
						
							|  |  |  | from system.swaglog import cloudlog
 | 
					
						
							|  |  |  | from selfdrive.controls.lib.alertmanager import set_offroad_alert
 | 
					
						
							|  |  |  | from system.version import is_tested_branch
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock")
 | 
					
						
							|  |  |  | STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | OVERLAY_UPPER = os.path.join(STAGING_ROOT, "upper")
 | 
					
						
							|  |  |  | OVERLAY_METADATA = os.path.join(STAGING_ROOT, "metadata")
 | 
					
						
							|  |  |  | OVERLAY_MERGED = os.path.join(STAGING_ROOT, "merged")
 | 
					
						
							|  |  |  | FINALIZED = os.path.join(STAGING_ROOT, "finalized")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | DAYS_NO_CONNECTIVITY_MAX = 14     # do not allow to engage after this many days
 | 
					
						
							|  |  |  | DAYS_NO_CONNECTIVITY_PROMPT = 10  # send an offroad prompt after this many days
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class WaitTimeHelper:
 | 
					
						
							|  |  |  |   def __init__(self, proc):
 | 
					
						
							|  |  |  |     self.proc = proc
 | 
					
						
							|  |  |  |     self.ready_event = threading.Event()
 | 
					
						
							|  |  |  |     self.shutdown = False
 | 
					
						
							|  |  |  |     signal.signal(signal.SIGTERM, self.graceful_shutdown)
 | 
					
						
							|  |  |  |     signal.signal(signal.SIGINT, self.graceful_shutdown)
 | 
					
						
							|  |  |  |     signal.signal(signal.SIGHUP, self.update_now)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def graceful_shutdown(self, signum: int, frame) -> None:
 | 
					
						
							|  |  |  |     # umount -f doesn't appear effective in avoiding "device busy" on NEOS,
 | 
					
						
							|  |  |  |     # so don't actually die until the next convenient opportunity in main().
 | 
					
						
							|  |  |  |     cloudlog.info("caught SIGINT/SIGTERM, dismounting overlay at next opportunity")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # forward the signal to all our child processes
 | 
					
						
							|  |  |  |     child_procs = self.proc.children(recursive=True)
 | 
					
						
							|  |  |  |     for p in child_procs:
 | 
					
						
							|  |  |  |       p.send_signal(signum)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.shutdown = True
 | 
					
						
							|  |  |  |     self.ready_event.set()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def update_now(self, signum: int, frame) -> None:
 | 
					
						
							|  |  |  |     cloudlog.info("caught SIGHUP, running update check immediately")
 | 
					
						
							|  |  |  |     self.ready_event.set()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def sleep(self, t: float) -> None:
 | 
					
						
							|  |  |  |     self.ready_event.wait(timeout=t)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def run(cmd: List[str], cwd: Optional[str] = None, low_priority: bool = False):
 | 
					
						
							|  |  |  |   if low_priority:
 | 
					
						
							|  |  |  |     cmd = ["nice", "-n", "19"] + cmd
 | 
					
						
							|  |  |  |   return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8')
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def set_consistent_flag(consistent: bool) -> None:
 | 
					
						
							|  |  |  |   os.sync()
 | 
					
						
							|  |  |  |   consistent_file = Path(os.path.join(FINALIZED, ".overlay_consistent"))
 | 
					
						
							|  |  |  |   if consistent:
 | 
					
						
							|  |  |  |     consistent_file.touch()
 | 
					
						
							|  |  |  |   elif not consistent:
 | 
					
						
							|  |  |  |     consistent_file.unlink(missing_ok=True)
 | 
					
						
							|  |  |  |   os.sync()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def set_params(new_version: bool, failed_count: int, exception: Optional[str]) -> None:
 | 
					
						
							|  |  |  |   params = Params()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   params.put("UpdateFailedCount", str(failed_count))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   last_update = datetime.datetime.utcnow()
 | 
					
						
							|  |  |  |   if failed_count == 0:
 | 
					
						
							|  |  |  |     t = last_update.isoformat()
 | 
					
						
							|  |  |  |     params.put("LastUpdateTime", t.encode('utf8'))
 | 
					
						
							|  |  |  |   else:
 | 
					
						
							|  |  |  |     try:
 | 
					
						
							|  |  |  |       t = params.get("LastUpdateTime", encoding='utf8')
 | 
					
						
							|  |  |  |       last_update = datetime.datetime.fromisoformat(t)
 | 
					
						
							|  |  |  |     except (TypeError, ValueError):
 | 
					
						
							|  |  |  |       pass
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if exception is None:
 | 
					
						
							|  |  |  |     params.delete("LastUpdateException")
 | 
					
						
							|  |  |  |   else:
 | 
					
						
							|  |  |  |     params.put("LastUpdateException", exception)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Write out release notes for new versions
 | 
					
						
							|  |  |  |   if new_version:
 | 
					
						
							|  |  |  |     try:
 | 
					
						
							|  |  |  |       with open(os.path.join(FINALIZED, "RELEASES.md"), "rb") as f:
 | 
					
						
							|  |  |  |         r = f.read().split(b'\n\n', 1)[0]  # Slice latest release notes
 | 
					
						
							|  |  |  |       try:
 | 
					
						
							|  |  |  |         params.put("ReleaseNotes", MarkdownIt().render(r.decode("utf-8")))
 | 
					
						
							|  |  |  |       except Exception:
 | 
					
						
							|  |  |  |         params.put("ReleaseNotes", r + b"\n")
 | 
					
						
							|  |  |  |     except Exception:
 | 
					
						
							|  |  |  |       params.put("ReleaseNotes", "")
 | 
					
						
							|  |  |  |     params.put_bool("UpdateAvailable", True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Handle user prompt
 | 
					
						
							|  |  |  |   for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"):
 | 
					
						
							|  |  |  |     set_offroad_alert(alert, False)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   now = datetime.datetime.utcnow()
 | 
					
						
							|  |  |  |   dt = now - last_update
 | 
					
						
							|  |  |  |   if failed_count > 15 and exception is not None:
 | 
					
						
							|  |  |  |     if is_tested_branch():
 | 
					
						
							|  |  |  |       extra_text = "Ensure the software is correctly installed"
 | 
					
						
							|  |  |  |     else:
 | 
					
						
							|  |  |  |       extra_text = exception
 | 
					
						
							|  |  |  |     set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text)
 | 
					
						
							|  |  |  |   elif dt.days > DAYS_NO_CONNECTIVITY_MAX and failed_count > 1:
 | 
					
						
							|  |  |  |     set_offroad_alert("Offroad_ConnectivityNeeded", True)
 | 
					
						
							|  |  |  |   elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT:
 | 
					
						
							|  |  |  |     remaining = max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 1)
 | 
					
						
							|  |  |  |     set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} day{'' if remaining == 1 else 's'}.")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def setup_git_options(cwd: str) -> None:
 | 
					
						
							|  |  |  |   # We sync FS object atimes (which NEOS doesn't use) and mtimes, but ctimes
 | 
					
						
							|  |  |  |   # are outside user control. Make sure Git is set up to ignore system ctimes,
 | 
					
						
							|  |  |  |   # because they change when we make hard links during finalize. Otherwise,
 | 
					
						
							|  |  |  |   # there is a lot of unnecessary churn. This appears to be a common need on
 | 
					
						
							|  |  |  |   # OSX as well: https://www.git-tower.com/blog/make-git-rebase-safe-on-osx/
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # We are using copytree to copy the directory, which also changes
 | 
					
						
							|  |  |  |   # inode numbers. Ignore those changes too.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Set protocol to the new version (default after git 2.26) to reduce data
 | 
					
						
							|  |  |  |   # usage on git fetch --dry-run from about 400KB to 18KB.
 | 
					
						
							|  |  |  |   git_cfg = [
 | 
					
						
							|  |  |  |     ("core.trustctime", "false"),
 | 
					
						
							|  |  |  |     ("core.checkStat", "minimal"),
 | 
					
						
							|  |  |  |     ("protocol.version", "2"),
 | 
					
						
							|  |  |  |     ("gc.auto", "0"),
 | 
					
						
							|  |  |  |     ("gc.autoDetach", "false"),
 | 
					
						
							|  |  |  |   ]
 | 
					
						
							|  |  |  |   for option, value in git_cfg:
 | 
					
						
							|  |  |  |     run(["git", "config", option, value], cwd)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def dismount_overlay() -> None:
 | 
					
						
							|  |  |  |   if os.path.ismount(OVERLAY_MERGED):
 | 
					
						
							|  |  |  |     cloudlog.info("unmounting existing overlay")
 | 
					
						
							|  |  |  |     run(["sudo", "umount", "-l", OVERLAY_MERGED])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def init_overlay() -> None:
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   overlay_init_file = Path(os.path.join(BASEDIR, ".overlay_init"))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Re-create the overlay if BASEDIR/.git has changed since we created the overlay
 | 
					
						
							|  |  |  |   if overlay_init_file.is_file():
 | 
					
						
							|  |  |  |     git_dir_path = os.path.join(BASEDIR, ".git")
 | 
					
						
							|  |  |  |     new_files = run(["find", git_dir_path, "-newer", str(overlay_init_file)])
 | 
					
						
							|  |  |  |     if not len(new_files.splitlines()):
 | 
					
						
							|  |  |  |       # A valid overlay already exists
 | 
					
						
							|  |  |  |       return
 | 
					
						
							|  |  |  |     else:
 | 
					
						
							|  |  |  |       cloudlog.info(".git directory changed, recreating overlay")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.info("preparing new safe staging area")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   params = Params()
 | 
					
						
							|  |  |  |   params.put_bool("UpdateAvailable", False)
 | 
					
						
							|  |  |  |   set_consistent_flag(False)
 | 
					
						
							|  |  |  |   dismount_overlay()
 | 
					
						
							|  |  |  |   run(["sudo", "rm", "-rf", STAGING_ROOT])
 | 
					
						
							|  |  |  |   if os.path.isdir(STAGING_ROOT):
 | 
					
						
							|  |  |  |     shutil.rmtree(STAGING_ROOT)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for dirname in [STAGING_ROOT, OVERLAY_UPPER, OVERLAY_METADATA, OVERLAY_MERGED]:
 | 
					
						
							|  |  |  |     os.mkdir(dirname, 0o755)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if os.lstat(BASEDIR).st_dev != os.lstat(OVERLAY_MERGED).st_dev:
 | 
					
						
							|  |  |  |     raise RuntimeError("base and overlay merge directories are on different filesystems; not valid for overlay FS!")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Leave a timestamped canary in BASEDIR to check at startup. The device clock
 | 
					
						
							|  |  |  |   # should be correct by the time we get here. If the init file disappears, or
 | 
					
						
							|  |  |  |   # critical mtimes in BASEDIR are newer than .overlay_init, continue.sh can
 | 
					
						
							|  |  |  |   # assume that BASEDIR has used for local development or otherwise modified,
 | 
					
						
							|  |  |  |   # and skips the update activation attempt.
 | 
					
						
							|  |  |  |   consistent_file = Path(os.path.join(BASEDIR, ".overlay_consistent"))
 | 
					
						
							|  |  |  |   if consistent_file.is_file():
 | 
					
						
							|  |  |  |     consistent_file.unlink()
 | 
					
						
							|  |  |  |   overlay_init_file.touch()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   os.sync()
 | 
					
						
							|  |  |  |   overlay_opts = f"lowerdir={BASEDIR},upperdir={OVERLAY_UPPER},workdir={OVERLAY_METADATA}"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   mount_cmd = ["mount", "-t", "overlay", "-o", overlay_opts, "none", OVERLAY_MERGED]
 | 
					
						
							|  |  |  |   run(["sudo"] + mount_cmd)
 | 
					
						
							|  |  |  |   run(["sudo", "chmod", "755", os.path.join(OVERLAY_METADATA, "work")])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   git_diff = run(["git", "diff"], OVERLAY_MERGED, low_priority=True)
 | 
					
						
							|  |  |  |   params.put("GitDiff", git_diff)
 | 
					
						
							|  |  |  |   cloudlog.info(f"git diff output:\n{git_diff}")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def finalize_update(wait_helper: WaitTimeHelper) -> None:
 | 
					
						
							|  |  |  |   """Take the current OverlayFS merged view and finalize a copy outside of
 | 
					
						
							|  |  |  |   OverlayFS, ready to be swapped-in at BASEDIR. Copy using shutil.copytree"""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Remove the update ready flag and any old updates
 | 
					
						
							|  |  |  |   cloudlog.info("creating finalized version of the overlay")
 | 
					
						
							|  |  |  |   set_consistent_flag(False)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Copy the merged overlay view and set the update ready flag
 | 
					
						
							|  |  |  |   if os.path.exists(FINALIZED):
 | 
					
						
							|  |  |  |     shutil.rmtree(FINALIZED)
 | 
					
						
							|  |  |  |   shutil.copytree(OVERLAY_MERGED, FINALIZED, symlinks=True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   run(["git", "reset", "--hard"], FINALIZED)
 | 
					
						
							|  |  |  |   run(["git", "submodule", "foreach", "--recursive", "git", "reset"], FINALIZED)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.info("Starting git gc")
 | 
					
						
							|  |  |  |   t = time.monotonic()
 | 
					
						
							|  |  |  |   try:
 | 
					
						
							|  |  |  |     run(["git", "gc"], FINALIZED)
 | 
					
						
							|  |  |  |     cloudlog.event("Done git gc", duration=time.monotonic() - t)
 | 
					
						
							|  |  |  |   except subprocess.CalledProcessError:
 | 
					
						
							|  |  |  |     cloudlog.exception(f"Failed git gc, took {time.monotonic() - t:.3f} s")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if wait_helper.shutdown:
 | 
					
						
							|  |  |  |     cloudlog.info("got interrupted finalizing overlay")
 | 
					
						
							|  |  |  |   else:
 | 
					
						
							|  |  |  |     set_consistent_flag(True)
 | 
					
						
							|  |  |  |     cloudlog.info("done finalizing overlay")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def handle_agnos_update(wait_helper: WaitTimeHelper) -> None:
 | 
					
						
							|  |  |  |   from system.hardware.tici.agnos import flash_agnos_update, get_target_slot_number
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cur_version = HARDWARE.get_os_version()
 | 
					
						
							|  |  |  |   updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \
 | 
					
						
							|  |  |  |                           echo -n $AGNOS_VERSION"], OVERLAY_MERGED).strip()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.info(f"AGNOS version check: {cur_version} vs {updated_version}")
 | 
					
						
							|  |  |  |   if cur_version == updated_version:
 | 
					
						
							|  |  |  |     return
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # prevent an openpilot getting swapped in with a mismatched or partially downloaded agnos
 | 
					
						
							|  |  |  |   set_consistent_flag(False)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.info(f"Beginning background installation for AGNOS {updated_version}")
 | 
					
						
							|  |  |  |   set_offroad_alert("Offroad_NeosUpdate", True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json")
 | 
					
						
							|  |  |  |   target_slot_number = get_target_slot_number()
 | 
					
						
							|  |  |  |   flash_agnos_update(manifest_path, target_slot_number, cloudlog)
 | 
					
						
							|  |  |  |   set_offroad_alert("Offroad_NeosUpdate", False)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_git_fetch_result(fetch_txt: str) -> bool:
 | 
					
						
							|  |  |  |   err_msg = "Failed to add the host to the list of known hosts (/data/data/com.termux/files/home/.ssh/known_hosts).\n"
 | 
					
						
							|  |  |  |   return len(fetch_txt) > 0 and (fetch_txt != err_msg)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def check_for_update() -> Tuple[bool, bool]:
 | 
					
						
							|  |  |  |   setup_git_options(OVERLAY_MERGED)
 | 
					
						
							|  |  |  |   try:
 | 
					
						
							|  |  |  |     git_fetch_output = run(["git", "fetch", "--dry-run"], OVERLAY_MERGED, low_priority=True)
 | 
					
						
							|  |  |  |     return True, check_git_fetch_result(git_fetch_output)
 | 
					
						
							|  |  |  |   except subprocess.CalledProcessError:
 | 
					
						
							|  |  |  |     return False, False
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def fetch_update(wait_helper: WaitTimeHelper) -> bool:
 | 
					
						
							|  |  |  |   cloudlog.info("attempting git fetch inside staging overlay")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setup_git_options(OVERLAY_MERGED)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   git_fetch_output = run(["git", "fetch"], OVERLAY_MERGED, low_priority=True)
 | 
					
						
							|  |  |  |   cloudlog.info("git fetch success: %s", git_fetch_output)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cur_hash = run(["git", "rev-parse", "HEAD"], OVERLAY_MERGED).rstrip()
 | 
					
						
							|  |  |  |   upstream_hash = run(["git", "rev-parse", "@{u}"], OVERLAY_MERGED).rstrip()
 | 
					
						
							|  |  |  |   new_version: bool = cur_hash != upstream_hash
 | 
					
						
							|  |  |  |   git_fetch_result = check_git_fetch_result(git_fetch_output)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cloudlog.info(f"comparing {cur_hash} to {upstream_hash}")
 | 
					
						
							|  |  |  |   if new_version or git_fetch_result:
 | 
					
						
							|  |  |  |     cloudlog.info("Running update")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if new_version:
 | 
					
						
							|  |  |  |       cloudlog.info("git reset in progress")
 | 
					
						
							|  |  |  |       r = [
 | 
					
						
							|  |  |  |         run(["git", "reset", "--hard", "@{u}"], OVERLAY_MERGED, low_priority=True),
 | 
					
						
							|  |  |  |         run(["git", "clean", "-xdf"], OVERLAY_MERGED, low_priority=True ),
 | 
					
						
							|  |  |  |         run(["git", "submodule", "init"], OVERLAY_MERGED, low_priority=True),
 | 
					
						
							|  |  |  |         run(["git", "submodule", "update"], OVERLAY_MERGED, low_priority=True),
 | 
					
						
							|  |  |  |       ]
 | 
					
						
							|  |  |  |       cloudlog.info("git reset success: %s", '\n'.join(r))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if AGNOS:
 | 
					
						
							|  |  |  |         handle_agnos_update(wait_helper)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Create the finalized, ready-to-swap update
 | 
					
						
							|  |  |  |     finalize_update(wait_helper)
 | 
					
						
							|  |  |  |     cloudlog.info("openpilot update successful!")
 | 
					
						
							|  |  |  |   else:
 | 
					
						
							|  |  |  |     cloudlog.info("nothing new from git at this time")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return new_version
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def main() -> None:
 | 
					
						
							|  |  |  |   params = Params()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if params.get_bool("DisableUpdates"):
 | 
					
						
							|  |  |  |     cloudlog.warning("updates are disabled by the DisableUpdates param")
 | 
					
						
							|  |  |  |     exit(0)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   ov_lock_fd = open(LOCK_FILE, 'w')
 | 
					
						
							|  |  |  |   try:
 | 
					
						
							|  |  |  |     fcntl.flock(ov_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
 | 
					
						
							|  |  |  |   except OSError as e:
 | 
					
						
							|  |  |  |     raise RuntimeError("couldn't get overlay lock; is another instance running?") from e
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Set low io priority
 | 
					
						
							|  |  |  |   proc = psutil.Process()
 | 
					
						
							|  |  |  |   if psutil.LINUX:
 | 
					
						
							|  |  |  |     proc.ionice(psutil.IOPRIO_CLASS_BE, value=7)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Check if we just performed an update
 | 
					
						
							|  |  |  |   if Path(os.path.join(STAGING_ROOT, "old_openpilot")).is_dir():
 | 
					
						
							|  |  |  |     cloudlog.event("update installed")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if not params.get("InstallDate"):
 | 
					
						
							|  |  |  |     t = datetime.datetime.utcnow().isoformat()
 | 
					
						
							|  |  |  |     params.put("InstallDate", t.encode('utf8'))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   overlay_init = Path(os.path.join(BASEDIR, ".overlay_init"))
 | 
					
						
							|  |  |  |   overlay_init.unlink(missing_ok=True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   update_failed_count = 0  # TODO: Load from param?
 | 
					
						
							|  |  |  |   wait_helper = WaitTimeHelper(proc)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Run the update loop
 | 
					
						
							|  |  |  |   while not wait_helper.shutdown:
 | 
					
						
							|  |  |  |     wait_helper.ready_event.clear()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Attempt an update
 | 
					
						
							|  |  |  |     exception = None
 | 
					
						
							|  |  |  |     new_version = False
 | 
					
						
							|  |  |  |     update_failed_count += 1
 | 
					
						
							|  |  |  |     try:
 | 
					
						
							|  |  |  |       init_overlay()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # TODO: still needed? skip this and just fetch?
 | 
					
						
							|  |  |  |       # Lightweight internt check
 | 
					
						
							|  |  |  |       internet_ok, update_available = check_for_update()
 | 
					
						
							|  |  |  |       if internet_ok and not update_available:
 | 
					
						
							|  |  |  |         update_failed_count = 0
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Fetch update
 | 
					
						
							|  |  |  |       if internet_ok:
 | 
					
						
							|  |  |  |         new_version = fetch_update(wait_helper)
 | 
					
						
							|  |  |  |         update_failed_count = 0
 | 
					
						
							|  |  |  |     except subprocess.CalledProcessError as e:
 | 
					
						
							|  |  |  |       cloudlog.event(
 | 
					
						
							|  |  |  |         "update process failed",
 | 
					
						
							|  |  |  |         cmd=e.cmd,
 | 
					
						
							|  |  |  |         output=e.output,
 | 
					
						
							|  |  |  |         returncode=e.returncode
 | 
					
						
							|  |  |  |       )
 | 
					
						
							|  |  |  |       exception = f"command failed: {e.cmd}\n{e.output}"
 | 
					
						
							|  |  |  |       overlay_init.unlink(missing_ok=True)
 | 
					
						
							|  |  |  |     except Exception as e:
 | 
					
						
							|  |  |  |       cloudlog.exception("uncaught updated exception, shouldn't happen")
 | 
					
						
							|  |  |  |       exception = str(e)
 | 
					
						
							|  |  |  |       overlay_init.unlink(missing_ok=True)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not wait_helper.shutdown:
 | 
					
						
							|  |  |  |       try:
 | 
					
						
							|  |  |  |         set_params(new_version, update_failed_count, exception)
 | 
					
						
							|  |  |  |       except Exception:
 | 
					
						
							|  |  |  |         cloudlog.exception("uncaught updated exception while setting params, shouldn't happen")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # infrequent attempts if we successfully updated recently
 | 
					
						
							|  |  |  |     wait_helper.sleep(5*60 if update_failed_count > 0 else 90*60)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   dismount_overlay()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__":
 | 
					
						
							|  |  |  |   main()
 |