You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					510 lines
				
				18 KiB
			
		
		
			
		
	
	
					510 lines
				
				18 KiB
			| 
											6 years ago
										 | #!/usr/bin/env python3
 | ||
|  | import os
 | ||
| 
											2 years ago
										 | import re
 | ||
| 
											6 years ago
										 | import datetime
 | ||
|  | import subprocess
 | ||
|  | import psutil
 | ||
| 
											2 years ago
										 | import shutil
 | ||
| 
											6 years ago
										 | import signal
 | ||
|  | import fcntl
 | ||
| 
											2 years ago
										 | import time
 | ||
| 
											6 years ago
										 | import threading
 | ||
| 
											2 years ago
										 | from collections import defaultdict
 | ||
|  | from pathlib import Path
 | ||
|  | from markdown_it import MarkdownIt
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 | from openpilot.common.basedir import BASEDIR
 | ||
| 
											2 years ago
										 | from openpilot.common.params import Params
 | ||
|  | from openpilot.common.time import system_time_valid
 | ||
| 
											2 years ago
										 | from openpilot.common.swaglog import cloudlog
 | ||
| 
											2 years ago
										 | from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
 | ||
| 
											2 years ago
										 | from openpilot.system.hardware import AGNOS, HARDWARE
 | ||
| 
											2 years ago
										 | from openpilot.system.version import get_build_metadata
 | ||
| 
											2 years ago
										 | 
 | ||
|  | 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")
 | ||
|  | 
 | ||
|  | OVERLAY_INIT = Path(os.path.join(BASEDIR, ".overlay_init"))
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											4 years ago
										 | 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
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 | class UserRequest:
 | ||
|  |   NONE = 0
 | ||
|  |   CHECK = 1
 | ||
|  |   FETCH = 2
 | ||
|  | 
 | ||
| 
											6 years ago
										 | class WaitTimeHelper:
 | ||
| 
											3 years ago
										 |   def __init__(self):
 | ||
| 
											5 years ago
										 |     self.ready_event = threading.Event()
 | ||
| 
											2 years ago
										 |     self.user_request = UserRequest.NONE
 | ||
| 
											6 years ago
										 |     signal.signal(signal.SIGHUP, self.update_now)
 | ||
| 
											3 years ago
										 |     signal.signal(signal.SIGUSR1, self.check_now)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											5 years ago
										 |   def update_now(self, signum: int, frame) -> None:
 | ||
| 
											3 years ago
										 |     cloudlog.info("caught SIGHUP, attempting to downloading update")
 | ||
| 
											2 years ago
										 |     self.user_request = UserRequest.FETCH
 | ||
| 
											3 years ago
										 |     self.ready_event.set()
 | ||
|  | 
 | ||
|  |   def check_now(self, signum: int, frame) -> None:
 | ||
|  |     cloudlog.info("caught SIGUSR1, checking for updates")
 | ||
| 
											2 years ago
										 |     self.user_request = UserRequest.CHECK
 | ||
| 
											6 years ago
										 |     self.ready_event.set()
 | ||
|  | 
 | ||
| 
											5 years ago
										 |   def sleep(self, t: float) -> None:
 | ||
| 
											5 years ago
										 |     self.ready_event.wait(timeout=t)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 | def write_time_to_param(params, param) -> None:
 | ||
|  |   t = datetime.datetime.utcnow()
 | ||
|  |   params.put(param, t.isoformat().encode('utf8'))
 | ||
|  | 
 | ||
| 
											2 years ago
										 | def read_time_from_param(params, param) -> datetime.datetime | None:
 | ||
| 
											2 years ago
										 |   t = params.get(param, encoding='utf8')
 | ||
|  |   try:
 | ||
|  |     return datetime.datetime.fromisoformat(t)
 | ||
|  |   except (TypeError, ValueError):
 | ||
|  |     pass
 | ||
|  |   return None
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 | def run(cmd: list[str], cwd: str = None) -> str:
 | ||
|  |   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 parse_release_notes(basedir: str) -> bytes:
 | ||
|  |   try:
 | ||
|  |     with open(os.path.join(basedir, "RELEASES.md"), "rb") as f:
 | ||
|  |       r = f.read().split(b'\n\n', 1)[0]  # Slice latest release notes
 | ||
|  |     try:
 | ||
|  |       return bytes(MarkdownIt().render(r.decode("utf-8")), encoding="utf-8")
 | ||
|  |     except Exception:
 | ||
|  |       return r + b"\n"
 | ||
|  |   except FileNotFoundError:
 | ||
|  |     pass
 | ||
|  |   except Exception:
 | ||
|  |     cloudlog.exception("failed to parse release notes")
 | ||
|  |   return b""
 | ||
|  | 
 | ||
|  | 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:
 | ||
|  | 
 | ||
|  |   # Re-create the overlay if BASEDIR/.git has changed since we created the overlay
 | ||
|  |   if OVERLAY_INIT.is_file() and os.path.ismount(OVERLAY_MERGED):
 | ||
|  |     git_dir_path = os.path.join(BASEDIR, ".git")
 | ||
|  |     new_files = run(["find", git_dir_path, "-newer", str(OVERLAY_INIT)])
 | ||
|  |     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.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)
 | ||
|  |   params.put("GitDiff", git_diff)
 | ||
|  |   cloudlog.info(f"git diff output:\n{git_diff}")
 | ||
|  | 
 | ||
|  | 
 | ||
|  | def finalize_update() -> 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", "--hard"], FINALIZED)
 | ||
|  | 
 | ||
|  |   cloudlog.info("Starting git cleanup in finalized update")
 | ||
|  |   t = time.monotonic()
 | ||
|  |   try:
 | ||
|  |     run(["git", "gc"], FINALIZED)
 | ||
|  |     run(["git", "lfs", "prune"], FINALIZED)
 | ||
|  |     cloudlog.event("Done git cleanup", duration=time.monotonic() - t)
 | ||
|  |   except subprocess.CalledProcessError:
 | ||
|  |     cloudlog.exception(f"Failed git cleanup, took {time.monotonic() - t:.3f} s")
 | ||
|  | 
 | ||
|  |   set_consistent_flag(True)
 | ||
|  |   cloudlog.info("done finalizing overlay")
 | ||
|  | 
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 | def handle_agnos_update() -> None:
 | ||
| 
											2 years ago
										 |   from openpilot.system.hardware.tici.agnos import flash_agnos_update, get_target_slot_number
 | ||
| 
											4 years ago
										 | 
 | ||
| 
											5 years ago
										 |   cur_version = HARDWARE.get_os_version()
 | ||
|  |   updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \
 | ||
| 
											2 years ago
										 |                           echo -n $AGNOS_VERSION"], OVERLAY_MERGED).strip()
 | ||
| 
											5 years ago
										 | 
 | ||
|  |   cloudlog.info(f"AGNOS version check: {cur_version} vs {updated_version}")
 | ||
|  |   if cur_version == updated_version:
 | ||
|  |     return
 | ||
|  | 
 | ||
| 
											5 years ago
										 |   # prevent an openpilot getting swapped in with a mismatched or partially downloaded agnos
 | ||
|  |   set_consistent_flag(False)
 | ||
|  | 
 | ||
| 
											5 years ago
										 |   cloudlog.info(f"Beginning background installation for AGNOS {updated_version}")
 | ||
| 
											5 years ago
										 |   set_offroad_alert("Offroad_NeosUpdate", True)
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											2 years ago
										 |   manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json")
 | ||
| 
											4 years ago
										 |   target_slot_number = get_target_slot_number()
 | ||
|  |   flash_agnos_update(manifest_path, target_slot_number, cloudlog)
 | ||
| 
											5 years ago
										 |   set_offroad_alert("Offroad_NeosUpdate", False)
 | ||
| 
											5 years ago
										 | 
 | ||
|  | 
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											3 years ago
										 | class Updater:
 | ||
|  |   def __init__(self):
 | ||
|  |     self.params = Params()
 | ||
| 
											2 years ago
										 |     self.branches = defaultdict(str)
 | ||
| 
											3 years ago
										 |     self._has_internet: bool = False
 | ||
|  | 
 | ||
|  |   @property
 | ||
|  |   def has_internet(self) -> bool:
 | ||
|  |     return self._has_internet
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |   @property
 | ||
|  |   def target_branch(self) -> str:
 | ||
|  |     b: str | None = self.params.get("UpdaterTargetBranch", encoding='utf-8')
 | ||
|  |     if b is None:
 | ||
|  |       b = self.get_branch(BASEDIR)
 | ||
|  |     return b
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |   @property
 | ||
|  |   def update_ready(self) -> bool:
 | ||
|  |     consistent_file = Path(os.path.join(FINALIZED, ".overlay_consistent"))
 | ||
|  |     if consistent_file.is_file():
 | ||
|  |       hash_mismatch = self.get_commit_hash(BASEDIR) != self.branches[self.target_branch]
 | ||
|  |       branch_mismatch = self.get_branch(BASEDIR) != self.target_branch
 | ||
|  |       on_target_branch = self.get_branch(FINALIZED) == self.target_branch
 | ||
|  |       return ((hash_mismatch or branch_mismatch) and on_target_branch)
 | ||
|  |     return False
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |   @property
 | ||
|  |   def update_available(self) -> bool:
 | ||
|  |     if os.path.isdir(OVERLAY_MERGED) and len(self.branches) > 0:
 | ||
|  |       hash_mismatch = self.get_commit_hash(OVERLAY_MERGED) != self.branches[self.target_branch]
 | ||
|  |       branch_mismatch = self.get_branch(OVERLAY_MERGED) != self.target_branch
 | ||
|  |       return hash_mismatch or branch_mismatch
 | ||
|  |     return False
 | ||
|  | 
 | ||
|  |   def get_branch(self, path: str) -> str:
 | ||
|  |     return run(["git", "rev-parse", "--abbrev-ref", "HEAD"], path).rstrip()
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |   def get_commit_hash(self, path: str = OVERLAY_MERGED) -> str:
 | ||
|  |     return run(["git", "rev-parse", "HEAD"], path).rstrip()
 | ||
| 
											2 years ago
										 | 
 | ||
| 
											2 years ago
										 |   def set_params(self, update_success: bool, failed_count: int, exception: str | None) -> None:
 | ||
|  |     self.params.put("UpdateFailedCount", str(failed_count))
 | ||
|  |     self.params.put("UpdaterTargetBranch", self.target_branch)
 | ||
| 
											2 years ago
										 | 
 | ||
| 
											2 years ago
										 |     self.params.put_bool("UpdaterFetchAvailable", self.update_available)
 | ||
|  |     if len(self.branches):
 | ||
|  |       self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys()))
 | ||
| 
											3 years ago
										 | 
 | ||
|  |     last_update = datetime.datetime.utcnow()
 | ||
| 
											2 years ago
										 |     if update_success:
 | ||
| 
											2 years ago
										 |       write_time_to_param(self.params, "LastUpdateTime")
 | ||
| 
											3 years ago
										 |     else:
 | ||
| 
											2 years ago
										 |       t = read_time_from_param(self.params, "LastUpdateTime")
 | ||
|  |       if t is not None:
 | ||
|  |         last_update = t
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											3 years ago
										 |     if exception is None:
 | ||
|  |       self.params.remove("LastUpdateException")
 | ||
|  |     else:
 | ||
|  |       self.params.put("LastUpdateException", exception)
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # Write out current and new version info
 | ||
|  |     def get_description(basedir: str) -> str:
 | ||
|  |       if not os.path.exists(basedir):
 | ||
|  |         return ""
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |       version = ""
 | ||
|  |       branch = ""
 | ||
|  |       commit = ""
 | ||
|  |       commit_date = ""
 | ||
|  |       try:
 | ||
|  |         branch = self.get_branch(basedir)
 | ||
|  |         commit = self.get_commit_hash(basedir)[:7]
 | ||
|  |         with open(os.path.join(basedir, "common", "version.h")) as f:
 | ||
|  |           version = f.read().split('"')[1]
 | ||
|  | 
 | ||
|  |         commit_unix_ts = run(["git", "show", "-s", "--format=%ct", "HEAD"], basedir).rstrip()
 | ||
|  |         dt = datetime.datetime.fromtimestamp(int(commit_unix_ts))
 | ||
|  |         commit_date = dt.strftime("%b %d")
 | ||
|  |       except Exception:
 | ||
|  |         cloudlog.exception("updater.get_description")
 | ||
|  |       return f"{version} / {branch} / {commit} / {commit_date}"
 | ||
|  |     self.params.put("UpdaterCurrentDescription", get_description(BASEDIR))
 | ||
|  |     self.params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
 | ||
|  |     self.params.put("UpdaterNewDescription", get_description(FINALIZED))
 | ||
|  |     self.params.put("UpdaterNewReleaseNotes", parse_release_notes(FINALIZED))
 | ||
|  |     self.params.put_bool("UpdateAvailable", self.update_ready)
 | ||
| 
											3 years ago
										 | 
 | ||
|  |     # 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
 | ||
| 
											2 years ago
										 |     build_metadata = get_build_metadata()
 | ||
| 
											3 years ago
										 |     if failed_count > 15 and exception is not None and self.has_internet:
 | ||
| 
											2 years ago
										 |       if build_metadata.tested_channel:
 | ||
| 
											3 years ago
										 |         extra_text = "Ensure the software is correctly installed. Uninstall and re-install if this error persists."
 | ||
|  |       else:
 | ||
|  |         extra_text = exception
 | ||
|  |       set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text)
 | ||
| 
											2 years ago
										 |     elif failed_count > 0:
 | ||
|  |       if dt.days > DAYS_NO_CONNECTIVITY_MAX:
 | ||
|  |         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'}.")
 | ||
| 
											3 years ago
										 | 
 | ||
|  |   def check_for_update(self) -> None:
 | ||
|  |     cloudlog.info("checking for updates")
 | ||
|  | 
 | ||
| 
											2 years ago
										 |     excluded_branches = ('release2', 'release2-staging')
 | ||
|  | 
 | ||
|  |     try:
 | ||
|  |       run(["git", "ls-remote", "origin", "HEAD"], OVERLAY_MERGED)
 | ||
|  |       self._has_internet = True
 | ||
|  |     except subprocess.CalledProcessError:
 | ||
|  |       self._has_internet = False
 | ||
|  | 
 | ||
|  |     setup_git_options(OVERLAY_MERGED)
 | ||
|  |     output = run(["git", "ls-remote", "--heads"], OVERLAY_MERGED)
 | ||
|  | 
 | ||
|  |     self.branches = defaultdict(lambda: None)
 | ||
|  |     for line in output.split('\n'):
 | ||
|  |       ls_remotes_re = r'(?P<commit_sha>\b[0-9a-f]{5,40}\b)(\s+)(refs\/heads\/)(?P<branch_name>.*$)'
 | ||
|  |       x = re.fullmatch(ls_remotes_re, line.strip())
 | ||
|  |       if x is not None and x.group('branch_name') not in excluded_branches:
 | ||
|  |         self.branches[x.group('branch_name')] = x.group('commit_sha')
 | ||
|  | 
 | ||
|  |     cur_branch = self.get_branch(OVERLAY_MERGED)
 | ||
|  |     cur_commit = self.get_commit_hash(OVERLAY_MERGED)
 | ||
|  |     new_branch = self.target_branch
 | ||
|  |     new_commit = self.branches[new_branch]
 | ||
|  |     if (cur_branch, cur_commit) != (new_branch, new_commit):
 | ||
|  |       cloudlog.info(f"update available, {cur_branch} ({str(cur_commit)[:7]}) -> {new_branch} ({str(new_commit)[:7]})")
 | ||
|  |     else:
 | ||
|  |       cloudlog.info(f"up to date on {cur_branch} ({str(cur_commit)[:7]})")
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											3 years ago
										 |   def fetch_update(self) -> None:
 | ||
| 
											2 years ago
										 |     cloudlog.info("attempting git fetch inside staging overlay")
 | ||
|  | 
 | ||
| 
											3 years ago
										 |     self.params.put("UpdaterState", "downloading...")
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											3 years ago
										 |     # TODO: cleanly interrupt this and invalidate old update
 | ||
|  |     set_consistent_flag(False)
 | ||
|  |     self.params.put_bool("UpdateAvailable", False)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 |     setup_git_options(OVERLAY_MERGED)
 | ||
|  | 
 | ||
|  |     branch = self.target_branch
 | ||
|  |     git_fetch_output = run(["git", "fetch", "origin", branch], OVERLAY_MERGED)
 | ||
|  |     cloudlog.info("git fetch success: %s", git_fetch_output)
 | ||
|  | 
 | ||
|  |     cloudlog.info("git reset in progress")
 | ||
|  |     cmds = [
 | ||
|  |       ["git", "checkout", "--force", "--no-recurse-submodules", "-B", branch, "FETCH_HEAD"],
 | ||
|  |       ["git", "reset", "--hard"],
 | ||
|  |       ["git", "clean", "-xdff"],
 | ||
|  |       ["git", "submodule", "sync"],
 | ||
|  |       ["git", "submodule", "update", "--init", "--recursive"],
 | ||
|  |       ["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"],
 | ||
|  |     ]
 | ||
|  |     r = [run(cmd, OVERLAY_MERGED) for cmd in cmds]
 | ||
|  |     cloudlog.info("git reset success: %s", '\n'.join(r))
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											3 years ago
										 |     # TODO: show agnos download progress
 | ||
|  |     if AGNOS:
 | ||
| 
											2 years ago
										 |       handle_agnos_update()
 | ||
| 
											5 years ago
										 | 
 | ||
|  |     # Create the finalized, ready-to-swap update
 | ||
| 
											3 years ago
										 |     self.params.put("UpdaterState", "finalizing update...")
 | ||
| 
											2 years ago
										 |     finalize_update()
 | ||
| 
											3 years ago
										 |     cloudlog.info("finalize success!")
 | ||
| 
											6 years ago
										 | 
 | ||
|  | 
 | ||
| 
											4 years ago
										 | def main() -> None:
 | ||
| 
											6 years ago
										 |   params = Params()
 | ||
|  | 
 | ||
| 
											5 years ago
										 |   if params.get_bool("DisableUpdates"):
 | ||
| 
											4 years ago
										 |     cloudlog.warning("updates are disabled by the DisableUpdates param")
 | ||
|  |     exit(0)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 |   with open(LOCK_FILE, 'w') as ov_lock_fd:
 | ||
|  |     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
 | ||
| 
											4 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # Set low io priority
 | ||
|  |     proc = psutil.Process()
 | ||
|  |     if psutil.LINUX:
 | ||
|  |       proc.ionice(psutil.IOPRIO_CLASS_BE, value=7)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # Check if we just performed an update
 | ||
|  |     if Path(os.path.join(STAGING_ROOT, "old_openpilot")).is_dir():
 | ||
|  |       cloudlog.event("update installed")
 | ||
| 
											4 years ago
										 | 
 | ||
| 
											2 years ago
										 |     if not params.get("InstallDate"):
 | ||
|  |       t = datetime.datetime.utcnow().isoformat()
 | ||
|  |       params.put("InstallDate", t.encode('utf8'))
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 |     updater = Updater()
 | ||
|  |     update_failed_count = 0 # TODO: Load from param?
 | ||
|  |     wait_helper = WaitTimeHelper()
 | ||
| 
											4 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # invalidate old finalized update
 | ||
|  |     set_consistent_flag(False)
 | ||
| 
											3 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # set initial state
 | ||
|  |     params.put("UpdaterState", "idle")
 | ||
| 
											2 years ago
										 | 
 | ||
| 
											2 years ago
										 |     # Run the update loop
 | ||
|  |     first_run = True
 | ||
|  |     while True:
 | ||
|  |       wait_helper.ready_event.clear()
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											2 years ago
										 |       # Attempt an update
 | ||
|  |       exception = None
 | ||
|  |       try:
 | ||
|  |         # TODO: reuse overlay from previous updated instance if it looks clean
 | ||
| 
											2 years ago
										 |         init_overlay()
 | ||
| 
											2 years ago
										 | 
 | ||
|  |         # ensure we have some params written soon after startup
 | ||
|  |         updater.set_params(False, update_failed_count, exception)
 | ||
|  | 
 | ||
|  |         if not system_time_valid() or first_run:
 | ||
|  |           first_run = False
 | ||
|  |           wait_helper.sleep(60)
 | ||
|  |           continue
 | ||
|  | 
 | ||
|  |         update_failed_count += 1
 | ||
|  | 
 | ||
|  |         # check for update
 | ||
|  |         params.put("UpdaterState", "checking...")
 | ||
|  |         updater.check_for_update()
 | ||
|  | 
 | ||
|  |         # download update
 | ||
|  |         last_fetch = read_time_from_param(params, "UpdaterLastFetchTime")
 | ||
|  |         timed_out = last_fetch is None or (datetime.datetime.utcnow() - last_fetch > datetime.timedelta(days=3))
 | ||
|  |         user_requested_fetch = wait_helper.user_request == UserRequest.FETCH
 | ||
|  |         if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch:
 | ||
|  |           cloudlog.info("skipping fetch, connection metered")
 | ||
|  |         elif wait_helper.user_request == UserRequest.CHECK:
 | ||
|  |           cloudlog.info("skipping fetch, only checking")
 | ||
|  |         else:
 | ||
|  |           updater.fetch_update()
 | ||
|  |           write_time_to_param(params, "UpdaterLastFetchTime")
 | ||
|  |         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}"
 | ||
| 
											2 years ago
										 |         OVERLAY_INIT.unlink(missing_ok=True)
 | ||
| 
											2 years ago
										 |       except Exception as e:
 | ||
|  |         cloudlog.exception("uncaught updated exception, shouldn't happen")
 | ||
|  |         exception = str(e)
 | ||
| 
											2 years ago
										 |         OVERLAY_INIT.unlink(missing_ok=True)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											2 years ago
										 |       try:
 | ||
|  |         params.put("UpdaterState", "idle")
 | ||
|  |         update_successful = (update_failed_count == 0)
 | ||
|  |         updater.set_params(update_successful, update_failed_count, exception)
 | ||
|  |       except Exception:
 | ||
|  |         cloudlog.exception("uncaught updated exception while setting params, shouldn't happen")
 | ||
| 
											4 years ago
										 | 
 | ||
| 
											2 years ago
										 |       # infrequent attempts if we successfully updated recently
 | ||
|  |       wait_helper.user_request = UserRequest.NONE
 | ||
|  |       wait_helper.sleep(5*60 if update_failed_count > 0 else 1.5*60*60)
 | ||
| 
											6 years ago
										 | 
 | ||
| 
											5 years ago
										 | 
 | ||
| 
											6 years ago
										 | if __name__ == "__main__":
 | ||
|  |   main()
 |