diff --git a/starpilot/common/starpilot_functions.py b/starpilot/common/starpilot_functions.py index ff94af244..eacd92039 100644 --- a/starpilot/common/starpilot_functions.py +++ b/starpilot/common/starpilot_functions.py @@ -1,33 +1,91 @@ #!/usr/bin/env python3 import dataclasses import json -import requests +import os import threading import time from pathlib import Path from types import SimpleNamespace -from cereal import messaging from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid from openpilot.system.hardware import HARDWARE -from openpilot.system.version import get_build_metadata -from openpilot.starpilot.assets.theme_manager import ThemeManager -from openpilot.starpilot.common.starpilot_backups import backup_starpilot -from openpilot.starpilot.common.connect_server import sync_konik_dongle_id -from openpilot.starpilot.common.maps_catalog import normalize_schedule_value, sanitize_selected_locations_csv -from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file -from openpilot.starpilot.common.starpilot_utilities import get_starpilot_api_info, is_FrogsGoMoo, is_url_pingable, run_cmd -from openpilot.starpilot.common.starpilot_variables import ( - ERROR_LOGS_PATH, STARPILOT_API, FROGS_GO_MOO_PATH, HD_LOGS_PATH, KONIK_LOGS_PATH, MAPS_PATH, THEME_SAVE_PATH, - StarPilotVariables, get_starpilot_toggles -) +STARPILOT_API = os.getenv("STARPILOT_API", "https://frogpilot.com/api") +BOOT_LOGO_STATE_PATH = Path("/cache/starpilot_boot_logo_state_v1") + + +def _starpilot_data_root(): + if HARDWARE.get_device_type() == "pc": + from openpilot.system.hardware.hw import Paths + + return Path(Paths.comma_home()) / "starpilot" / "data" + return Path("/data") + + +def _starpilot_persist_root(): + if HARDWARE.get_device_type() == "pc": + from openpilot.system.hardware.hw import Paths + + return Path(Paths.persist_root()) + return Path("/persist") + + +def _theme_save_path(): + return _starpilot_data_root() / "themes" + + +def _maps_path(): + return _starpilot_data_root() / "media/0/osm/offline" + + +def _run_cmd(*args, **kwargs): + from openpilot.starpilot.common.starpilot_utilities import run_cmd + + return run_cmd(*args, **kwargs) + + +def _boot_logo_cache_key(selected_logo, source_logo): + try: + source_stat = source_logo.stat() + source_path = source_logo.resolve() + except OSError: + return None + + selected = selected_logo.decode("utf-8", "ignore") if isinstance(selected_logo, (bytes, bytearray)) else str(selected_logo or "") + return "\n".join([ + selected.strip().lower(), + str(source_path), + str(source_stat.st_mtime_ns), + str(source_stat.st_size), + "", + ]) + + +def _boot_logo_cache_matches(cache_key): + if cache_key is None: + return False + try: + return BOOT_LOGO_STATE_PATH.read_text() == cache_key + except OSError: + return False + + +def _write_boot_logo_cache(cache_key): + if cache_key is None: + return + try: + BOOT_LOGO_STATE_PATH.parent.mkdir(parents=True, exist_ok=True) + BOOT_LOGO_STATE_PATH.write_text(cache_key) + except OSError: + pass def seed_desktop_theme_assets(): + from openpilot.starpilot.assets.theme_manager import ThemeManager + params = Params() params_memory = Params(memory=True) params_defaults = Params(return_defaults=True) @@ -59,6 +117,11 @@ def seed_desktop_theme_assets(): def starpilot_boot_functions(build_metadata, params): + from openpilot.starpilot.assets.theme_manager import ThemeManager + from openpilot.starpilot.common.connect_server import sync_konik_dongle_id + from openpilot.starpilot.common.maps_catalog import sanitize_selected_locations_csv + from openpilot.starpilot.common.starpilot_variables import StarPilotVariables + params_memory = Params(memory=True) maps_selected_raw = params.get("MapsSelected") @@ -70,12 +133,14 @@ def starpilot_boot_functions(build_metadata, params): params.put("BuildMetadata", json.dumps(dataclasses.asdict(build_metadata))) - StarPilotVariables() - ThemeManager(params, params_memory, boot_run=True).update_active_theme(time_validated=system_time_valid(), starpilot_toggles=get_starpilot_toggles(), boot_run=True) + starpilot_toggles = StarPilotVariables().starpilot_toggles + ThemeManager(params, params_memory, boot_run=True).update_active_theme(time_validated=system_time_valid(), starpilot_toggles=starpilot_toggles, boot_run=True) sync_konik_dongle_id(params) def boot_thread(): + from openpilot.starpilot.common.starpilot_backups import backup_starpilot + while not system_time_valid(): print("Waiting for system time to become valid...") time.sleep(1) @@ -86,12 +151,13 @@ def starpilot_boot_functions(build_metadata, params): def install_starpilot(build_metadata, params): + data_root = _starpilot_data_root() paths = [ - ERROR_LOGS_PATH, - HD_LOGS_PATH, - KONIK_LOGS_PATH, - MAPS_PATH, - THEME_SAVE_PATH + data_root / "error_logs", + data_root / "media/0/realdata_HD", + data_root / "media/0/realdata_konik", + data_root / "media/0/osm/offline", + _theme_save_path() ] for path in paths: path.mkdir(parents=True, exist_ok=True) @@ -100,15 +166,27 @@ def install_starpilot(build_metadata, params): update_boot_logo(starpilot=True, selected_logo=params.get("BootLogo")) - if is_FrogsGoMoo(): - mount_options = run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/persist"], "Successfully retrieved mount options", "Failed to retrieve mount options") - run_cmd(["sudo", "mount", "-o", "remount,rw", "/persist"], "Successfully remounted /persist as read-write", "Failed to remount /persist") - run_cmd(["sudo", "python3", FROGS_GO_MOO_PATH], "Successfully ran frogsgomoo.py", "Failed to run frogsgomoo.py") - run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/persist"], "Successfully restored /persist mount options", "Failed to restore /persist mount options") + frogs_go_moo_path = _starpilot_persist_root() / "frogsgomoo.py" + if frogs_go_moo_path.is_file(): + mount_options = _run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/persist"], "Successfully retrieved mount options", "Failed to retrieve mount options") + _run_cmd(["sudo", "mount", "-o", "remount,rw", "/persist"], "Successfully remounted /persist as read-write", "Failed to remount /persist") + _run_cmd(["sudo", "python3", frogs_go_moo_path], "Successfully ran frogsgomoo.py", "Failed to run frogsgomoo.py") + _run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/persist"], "Successfully restored /persist mount options", "Failed to restore /persist mount options") def register_device(build_metadata, params): def register_thread(): + import requests + + def is_url_pingable(url): + if not url: + return False + try: + response = requests.head(url, timeout=10, allow_redirects=True, headers={"User-Agent": "starpilot-ping-test/1.0 (https://github.com/FrogAi/StarPilot)"}) + return response.ok + except Exception: + return False + dongle_id = params.get("DongleId") if isinstance(dongle_id, bytes): dongle_id = dongle_id.decode("utf-8", errors="ignore") @@ -163,7 +241,9 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): selected = selected_logo.decode("utf-8", "ignore") if isinstance(selected_logo, (bytes, bytearray)) else str(selected_logo) selected = selected.strip() if selected.lower() not in {"", "stock", "default"}: - matched_logo = find_matching_theme_asset_file(THEME_SAVE_PATH / "bootlogos", selected) + from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file + + matched_logo = find_matching_theme_asset_file(_theme_save_path() / "bootlogos", selected) if matched_logo is not None: target_logo = matched_logo elif stock: @@ -176,6 +256,10 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): print(f"Error: Target logo file not found at {target_logo}") return + cache_key = _boot_logo_cache_key(selected_logo, target_logo) + if boot_logo_location.is_file() and _boot_logo_cache_matches(cache_key): + return + source_logo = target_logo staged_logo = Path("/tmp/starpilot_boot_logo.jpg") try: @@ -196,13 +280,18 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): current_logo = boot_logo_location.read_bytes() if boot_logo_location.is_file() else b"" desired_logo = source_logo.read_bytes() if current_logo != desired_logo: - mount_options = run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/"], "Successfully retrieved mount options", "Failed to retrieve mount options") - run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount /") - run_cmd(["sudo", "cp", source_logo, boot_logo_location], "Successfully replaced boot logo", "Failed to replace boot logo") - run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options") + mount_options = _run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/"], "Successfully retrieved mount options", "Failed to retrieve mount options") + _run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount /") + if _run_cmd(["sudo", "cp", source_logo, boot_logo_location], "Successfully replaced boot logo", "Failed to replace boot logo") is None: + return + _run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options") + _write_boot_logo_cache(cache_key) def update_maps(now, params, params_memory, manual_update=False): + from cereal import messaging + from openpilot.starpilot.common.maps_catalog import normalize_schedule_value, sanitize_selected_locations_csv + maps_selected_raw = params.get("MapsSelected") maps_selected = sanitize_selected_locations_csv(maps_selected_raw) if not maps_selected: @@ -217,7 +306,8 @@ def update_maps(now, params, params_memory, manual_update=False): is_sunday = now.weekday() == 6 schedule = normalize_schedule_value(params.get("PreferredSchedule")) - maps_downloaded = MAPS_PATH.exists() and any(path.is_file() for path in MAPS_PATH.rglob("*")) + maps_path = _maps_path() + maps_downloaded = maps_path.exists() and any(path.is_file() for path in maps_path.rglob("*")) if maps_downloaded and (schedule == 0 or (schedule == 1 and not is_sunday) or (schedule == 2 and not is_first)) and not manual_update: return @@ -265,7 +355,7 @@ def update_maps(now, params, params_memory, manual_update=False): def update_openpilot(thread_manager, params): def update_available(): - run_cmd(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], "Checking for updates...", "Failed to check for update...", report=False) + _run_cmd(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], "Checking for updates...", "Failed to check for update...", report=False) while params.get("UpdaterState") != "checking...": time.sleep(1) @@ -279,7 +369,7 @@ def update_openpilot(thread_manager, params): while params.get_bool("IsOnroad") or thread_manager.is_thread_alive("lock_doors"): time.sleep(60) - run_cmd(["pkill", "-SIGHUP", "-f", "system.updated.updated"], "Update available, downloading...", "Failed to download update...", report=False) + _run_cmd(["pkill", "-SIGHUP", "-f", "system.updated.updated"], "Update available, downloading...", "Failed to download update...", report=False) while not params.get_bool("UpdateAvailable"): time.sleep(60) diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index fb91f55dc..7e14c4cae 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -316,7 +316,12 @@ def default_ev_tuning_enabled(CP): ev_vehicle |= getattr(CP, "transmissionType", None) == car.CarParams.TransmissionType.direct return bool(ev_vehicle) -def get_starpilot_toggles(sm=messaging.SubMaster(["starpilotPlan"])): +def get_starpilot_toggles(sm=None): + if sm is None: + if not hasattr(get_starpilot_toggles, "_sm"): + get_starpilot_toggles._sm = messaging.SubMaster(["starpilotPlan"]) + sm = get_starpilot_toggles._sm + toggles_text = sm["starpilotPlan"].starpilotToggles if toggles_text: get_starpilot_toggles._last_toggles_text = toggles_text diff --git a/system/athena/registration.py b/system/athena/registration.py index d20de70b9..759bd40f0 100644 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -1,29 +1,57 @@ #!/usr/bin/env python3 import time -import json -import jwt -import random -import string -from typing import cast from pathlib import Path -from datetime import datetime, timedelta, UTC -from openpilot.common.api import api_get, get_key_pair from openpilot.common.params import Params -from openpilot.common.spinner import Spinner -from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog UNREGISTERED_DONGLE_ID = "UnregisteredDevice" +OFFROAD_UNREGISTERED_ALERT = "Offroad_UnregisteredHardware" +KEYS = {"id_rsa": "RS256", + "id_ecdsa": "ES256"} + + +def api_get(*args, **kwargs): + from openpilot.common.api import api_get as api_get_impl + + return api_get_impl(*args, **kwargs) + + +def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + for key, algorithm in KEYS.items(): + private_path = Path(Paths.persist_root()) / "comma" / key + public_path = Path(Paths.persist_root()) / "comma" / f"{key}.pub" + if private_path.is_file() and public_path.is_file(): + return algorithm, private_path.read_text(), public_path.read_text() + return None, None, None + + +def _has_key_pair() -> bool: + for key in KEYS: + private_path = Path(Paths.persist_root()) / "comma" / key + public_path = Path(Paths.persist_root()) / "comma" / f"{key}.pub" + if private_path.is_file() and public_path.is_file(): + return True + return False def is_registered_device() -> bool: dongle = Params().get("DongleId") return dongle not in (None, UNREGISTERED_DONGLE_ID) +def _set_registration_alert(params: Params, dongle_id: str | None) -> None: + show_alert = (dongle_id == UNREGISTERED_DONGLE_ID) and not PC + if show_alert: + from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert + + set_offroad_alert(OFFROAD_UNREGISTERED_ALERT, True) + else: + params.remove(OFFROAD_UNREGISTERED_ALERT) + + def register(show_spinner=False, register_konik=False) -> str | None: """ All devices built since March 2024 come with all @@ -42,13 +70,18 @@ def register(show_spinner=False, register_konik=False) -> str | None: with open(Paths.persist_root()+"/comma/dongle_id") as f: dongle_id = f.read().strip() - # Create registration token, in the future, this key will make JWTs directly - jwt_algo, private_key, public_key = get_key_pair() - - if not public_key and not register_konik: + if not _has_key_pair() and not register_konik: dongle_id = UNREGISTERED_DONGLE_ID cloudlog.warning("missing public key") elif dongle_id is None or register_konik: + import json + import jwt + from datetime import datetime, timedelta, UTC + from openpilot.common.spinner import Spinner + + # Create registration token, in the future, this key will make JWTs directly + jwt_algo, private_key, public_key = get_key_pair() + if show_spinner: spinner = Spinner() spinner.update("registering device") @@ -73,14 +106,14 @@ def register(show_spinner=False, register_konik=False) -> str | None: while True: try: register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, - cast(str, private_key), algorithm=jwt_algo) + private_key, algorithm=jwt_algo) cloudlog.info("getting pilotauth") resp = api_get("v2/pilotauth/", method='POST', timeout=15, imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) if resp.status_code in (402, 403): cloudlog.info(f"Unable to register device, got {resp.status_code}") - dongle_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16)) + dongle_id = UNREGISTERED_DONGLE_ID else: dongleauth = json.loads(resp.text) dongle_id = dongleauth["dongle_id"] @@ -99,7 +132,7 @@ def register(show_spinner=False, register_konik=False) -> str | None: if not register_konik and dongle_id != params.get("KonikDongleId"): params.put("DongleId", dongle_id) params.put("StockDongleId", dongle_id) - set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) + _set_registration_alert(params, dongle_id) return dongle_id diff --git a/system/manager/helpers.py b/system/manager/helpers.py index 32fdd538a..a096f0687 100644 --- a/system/manager/helpers.py +++ b/system/manager/helpers.py @@ -1,18 +1,11 @@ -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 def unblock_stdout() -> None: + import errno + import fcntl + import signal + import sys + # get a non-blocking stdout child_pid, child_pty = os.forkpty() if child_pid != 0: # parent @@ -51,7 +44,17 @@ def write_onroad_params(started, params): def save_bootlog(): + import threading + def fn(): + import pathlib + import shutil + import subprocess + import tempfile + + from openpilot.common.basedir import BASEDIR + from openpilot.common.params import Params + tmpdir = tempfile.mkdtemp() env = os.environ.copy() env['PARAMS_COPY_PATH'] = tmpdir diff --git a/system/manager/manager.py b/system/manager/manager.py index 995ef2d58..55cae6cb2 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -2,7 +2,6 @@ import datetime import json import os -import shutil from pathlib import Path import signal import sys @@ -24,26 +23,18 @@ def _append_boot_timing_line(line: str) -> None: from cereal import car, log import cereal.messaging as messaging -from openpilot.common.utils import atomic_write from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType -from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes -from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID -from openpilot.common.swaglog import cloudlog, add_file_handler +from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths _MANAGER_CORE_IMPORT_DONE = time.monotonic() from openpilot.starpilot.common.starpilot_functions import starpilot_boot_functions, install_starpilot, uninstall_starpilot -from openpilot.starpilot.common.starpilot_variables import ( - LEGACY_STARPILOT_PARAM_RENAMES, - LEGACY_STARPILOT_STATS_KEY_RENAMES, - get_starpilot_toggles, -) _MANAGER_IMPORT_DONE = time.monotonic() _manager_import_timing_line = ( @@ -67,9 +58,23 @@ STARPILOT_PC_ROOT_MIGRATION_FLAG = Path("/data") / "starpilot_pc_root_v1" STARPILOT_PARAMS_CACHE_MIGRATION_FLAG = Path("/data") / "starpilot_params_cache_v1" STARPILOT_LEGACY_CACHE_MARKER_KEYS = ("RemapCancelToDistance",) STARPILOT_REMOVED_PARAM_KEYS = ("HumanFollowing",) +UNREGISTERED_DONGLE_ID = "UnregisteredDevice" +POWER_WATCHDOG_PATH = "/var/tmp/power_watchdog" LEGACY_CARMODEL_MIGRATIONS = { "CHEVROLET_BOLT_CC_2019_2021": "CHEVROLET_BOLT_CC_2018_2021", } +LEGACY_STARPILOT_PARAM_RENAMES = { + "FrogPilotApiToken": "StarPilotApiToken", + "FrogPilotCarParams": "StarPilotCarParams", + "FrogPilotCarParamsPersistent": "StarPilotCarParamsPersistent", + "FrogPilotDongleId": "StarPilotDongleId", + "FrogPilotStats": "StarPilotStats", +} +LEGACY_STARPILOT_STATS_KEY_RENAMES = { + "FrogPilotDrives": "StarPilotDrives", + "FrogPilotMeters": "StarPilotMeters", + "FrogPilotSeconds": "StarPilotSeconds", +} STARPILOT_STATS_DROP_KEYS = {"CurrentMonthsKilometers", "ResetStats"} STARPILOT_STATS_MAX_KEYS = {"LongestDistanceWithoutOverride", "MaxAcceleration"} @@ -114,6 +119,30 @@ def _capture_manager_exception() -> None: cloudlog.exception("failed to capture manager exception") +def _write_power_watchdog(timestamp: float) -> None: + import tempfile + + tmp_file_name = None + try: + with tempfile.NamedTemporaryFile("w", dir=os.path.dirname(POWER_WATCHDOG_PATH), delete=False) as f: + tmp_file_name = f.name + f.write(str(timestamp)) + os.replace(tmp_file_name, POWER_WATCHDOG_PATH) + tmp_file_name = None + finally: + if tmp_file_name is not None: + try: + os.unlink(tmp_file_name) + except FileNotFoundError: + pass + + +def _get_starpilot_toggles(sm=None): + from openpilot.starpilot.common.starpilot_variables import get_starpilot_toggles + + return get_starpilot_toggles(sm) + + def _to_text(value): if value is None: return None @@ -285,6 +314,8 @@ def _copy_param_store_without_overwrite(source: Path, destination: Path) -> int: if not source.is_dir(): return 0 + import shutil + destination.mkdir(parents=True, exist_ok=True) copied_entries = 0 for path in source.iterdir(): @@ -402,6 +433,8 @@ def _merge_tree_without_overwrite(source: Path, destination: Path) -> int: if not source.exists(): return moved_entries + import shutil + if not destination.exists(): destination.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(source), str(destination)) @@ -881,6 +914,8 @@ def manager_init() -> None: last_timing = _log_boot_timing("manager_init", "version_params", manager_init_start, last_timing) # set dongle id + from openpilot.system.athena.registration import register + reg_res = register(show_spinner=True) if reg_res: dongle_id = reg_res @@ -958,7 +993,7 @@ def manager_thread() -> None: last_timing = _log_boot_timing("manager_thread", "messaging", manager_thread_start, last_timing) write_onroad_params(False, params) - initial_toggles = get_starpilot_toggles() + initial_toggles = _get_starpilot_toggles() last_timing = _log_boot_timing("manager_thread", "initial_toggles", manager_thread_start, last_timing) ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore, starpilot_toggles=initial_toggles) last_timing = _log_boot_timing("manager_thread", "initial_ensure_running", manager_thread_start, last_timing) @@ -974,7 +1009,7 @@ def manager_thread() -> None: params_memory = Params(memory=True) - starpilot_toggles = get_starpilot_toggles() + starpilot_toggles = _get_starpilot_toggles() last_timing = _log_boot_timing("manager_thread", "loop_toggles", manager_thread_start, last_timing) _log_boot_timing("manager_thread", "loop_ready", manager_thread_start, last_timing) @@ -1024,8 +1059,7 @@ def manager_thread() -> None: # 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())) + _write_power_watchdog(time.monotonic()) except Exception: pass @@ -1048,7 +1082,7 @@ def manager_thread() -> None: break # StarPilot variables - starpilot_toggles = get_starpilot_toggles(sm) + starpilot_toggles = _get_starpilot_toggles(sm) def main() -> None: @@ -1087,6 +1121,9 @@ if __name__ == "__main__": except KeyboardInterrupt: print("got CTRL-C, exiting") except Exception: + from openpilot.common.swaglog import add_file_handler + from openpilot.common.text_window import TextWindow + add_file_handler(cloudlog) cloudlog.exception("Manager failed to start")