#!/usr/bin/env python2.7 import os import sys import fcntl import errno import signal if __name__ == "__main__": if os.path.isfile("/init.qcom.rc") \ and (not os.path.isfile("/VERSION") or int(open("/VERSION").read()) < 4): raise Exception("NEOS outdated") # 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) except (OSError, IOError): pass os._exit(os.wait()[1]) import glob import shutil import hashlib import importlib import subprocess import traceback from multiprocessing import Process from common.basedir import BASEDIR sys.path.append(os.path.join(BASEDIR, "pyextra")) os.environ['BASEDIR'] = BASEDIR import zmq from setproctitle import setproctitle #pylint: disable=no-name-in-module from common.params import Params from common.realtime import sec_since_boot from selfdrive.services import service_list from selfdrive.swaglog import cloudlog import selfdrive.messaging as messaging from selfdrive.thermal import read_thermal from selfdrive.registration import register from selfdrive.version import version, dirty, training_version import selfdrive.crash as crash from selfdrive.loggerd.config import ROOT EON = os.path.exists("/EON") # comment out anything you don't want to run managed_processes = { "uploader": "selfdrive.loggerd.uploader", "controlsd": "selfdrive.controls.controlsd", "radard": "selfdrive.controls.radard", "ubloxd": "selfdrive.locationd.ubloxd", "locationd_dummy": "selfdrive.locationd.locationd_dummy", "loggerd": ("selfdrive/loggerd", ["./loggerd"]), "logmessaged": "selfdrive.logmessaged", "tombstoned": "selfdrive.tombstoned", "logcatd": ("selfdrive/logcatd", ["./logcatd"]), "proclogd": ("selfdrive/proclogd", ["./proclogd"]), "boardd": ("selfdrive/boardd", ["./boardd"]), # not used directly "pandad": "selfdrive.pandad", "ui": ("selfdrive/ui", ["./ui"]), "visiond": ("selfdrive/visiond", ["./visiond"]), "sensord": ("selfdrive/sensord", ["./sensord"]), "gpsd": ("selfdrive/sensord", ["./gpsd"]), "orbd": ("selfdrive/orbd", ["./orbd"]), "updated": "selfdrive.updated", #"gpsplanner": "selfdrive.controls.gps_plannerd", } running = {} def get_running(): return running # due to qualcomm kernel bugs SIGKILLing visiond sometimes causes page table corruption unkillable_processes = ['visiond'] # processes to end with SIGINT instead of SIGTERM interrupt_processes = [] persistent_processes = [ 'logmessaged', 'logcatd', 'tombstoned', 'uploader', 'ui', 'gpsd', 'ubloxd', 'locationd_dummy', 'updated', ] car_started_processes = [ 'controlsd', 'loggerd', 'sensord', 'radard', 'visiond', 'proclogd', 'orbd', # 'gpsplanner, ] def register_managed_process(name, desc, car_started=False): global managed_processes, car_started_processes, persistent_processes print "registering", name managed_processes[name] = desc if car_started: car_started_processes.append(name) else: persistent_processes.append(name) # ****************** process management functions ****************** def launcher(proc, gctx): try: # import the process mod = importlib.import_module(proc) # rename the process setproctitle(proc) # exec the process mod.main(gctx) except KeyboardInterrupt: cloudlog.warning("child %s got SIGINT" % proc) except Exception: # can't install the crash handler becuase sys.excepthook doesn't play nice # with threads, so catch it here. crash.capture_exception() raise def nativelauncher(pargs, cwd): # exec the process os.chdir(cwd) # because when extracted from pex zips permissions get lost -_- os.chmod(pargs[0], 0o700) os.execvp(pargs[0], pargs) def start_managed_process(name): if name in running or name not in managed_processes: return proc = managed_processes[name] if isinstance(proc, basestring): cloudlog.info("starting python %s" % proc) running[name] = Process(name=name, target=launcher, args=(proc, gctx)) else: pdir, pargs = proc cwd = os.path.join(BASEDIR, pdir) cloudlog.info("starting process %s" % name) running[name] = Process(name=name, target=nativelauncher, args=(pargs, cwd)) running[name].start() def prepare_managed_process(p): proc = managed_processes[p] if isinstance(proc, basestring): # import this python cloudlog.info("preimporting %s" % proc) importlib.import_module(proc) else: # build this process cloudlog.info("building %s" % (proc,)) try: subprocess.check_call(["make", "-j4"], cwd=os.path.join(BASEDIR, proc[0])) except subprocess.CalledProcessError: # make clean if the build failed cloudlog.warning("building %s failed, make clean" % (proc, )) subprocess.check_call(["make", "clean"], cwd=os.path.join(BASEDIR, proc[0])) subprocess.check_call(["make", "-j4"], cwd=os.path.join(BASEDIR, proc[0])) def kill_managed_process(name): if name not in running or name not in managed_processes: return cloudlog.info("killing %s" % name) if running[name].exitcode is None: if name in interrupt_processes: os.kill(running[name].pid, signal.SIGINT) else: running[name].terminate() # give it 5 seconds to die running[name].join(5.0) if running[name].exitcode is None: if name in unkillable_processes: cloudlog.critical("unkillable process %s failed to exit! rebooting in 15 if it doesn't die" % name) running[name].join(15.0) if running[name].exitcode is None: cloudlog.critical("FORCE REBOOTING PHONE!") os.system("date >> /sdcard/unkillable_reboot") os.system("reboot") raise RuntimeError else: cloudlog.info("killing %s with SIGKILL" % name) os.kill(running[name].pid, signal.SIGKILL) running[name].join() cloudlog.info("%s is dead with %d" % (name, running[name].exitcode)) del running[name] def cleanup_all_processes(signal, frame): cloudlog.info("caught ctrl-c %s %s" % (signal, frame)) for p in ("com.waze", "com.spotify.music", "ai.comma.plus.offroad", "ai.comma.plus.frame"): system("am force-stop %s" % p) for name in running.keys(): kill_managed_process(name) cloudlog.info("everything is dead") # ****************** run loop ****************** def manager_init(should_register=True): global gctx if should_register: reg_res = register() if reg_res: dongle_id, dongle_secret = reg_res else: raise Exception("server registration failed") else: dongle_id = "c"*16 # set dongle id cloudlog.info("dongle id is " + dongle_id) os.environ['DONGLE_ID'] = dongle_id cloudlog.info("dirty is %d" % dirty) if not dirty: os.environ['CLEAN'] = '1' cloudlog.bind_global(dongle_id=dongle_id, version=version, dirty=dirty, is_eon=EON) crash.bind_user(id=dongle_id) crash.bind_extra(version=version, dirty=dirty, is_eon=EON) os.umask(0) try: os.mkdir(ROOT, 0777) except OSError: pass # set gctx gctx = {} def system(cmd): try: cloudlog.info("running %s" % cmd) subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) except subprocess.CalledProcessError, e: cloudlog.event("running failed", cmd=e.cmd, output=e.output[-1024:], returncode=e.returncode) def setup_eon_fan(): if not EON: return os.system("echo 2 > /sys/module/dwc3_msm/parameters/otg_switch") from smbus2 import SMBus bus = SMBus(7, force=True) bus.write_byte_data(0x21, 0x10, 0xf) # mask all interrupts bus.write_byte_data(0x21, 0x03, 0x1) # set drive current and global interrupt disable bus.write_byte_data(0x21, 0x02, 0x2) # needed? bus.write_byte_data(0x21, 0x04, 0x4) # manual override source bus.close() last_eon_fan_val = None def set_eon_fan(val): global last_eon_fan_val if not EON: return from smbus2 import SMBus if last_eon_fan_val is None or last_eon_fan_val != val: bus = SMBus(7, force=True) bus.write_byte_data(0x21, 0x04, 0x2) bus.write_byte_data(0x21, 0x03, (val*2)+1) bus.write_byte_data(0x21, 0x04, 0x4) bus.close() last_eon_fan_val = val # temp thresholds to control fan speed - high hysteresis _TEMP_THRS_H = [50., 65., 80., 10000] # temp thresholds to control fan speed - low hysteresis _TEMP_THRS_L = [42.5, 57.5, 72.5, 10000] # fan speed options _FAN_SPEEDS = [0, 16384, 32768, 65535] # max fan speed only allowed if battery if hot _BAT_TEMP_THERSHOLD = 45. def handle_fan(max_temp, bat_temp, fan_speed): new_speed_h = next(speed for speed, temp_h in zip(_FAN_SPEEDS, _TEMP_THRS_H) if temp_h > max_temp) new_speed_l = next(speed for speed, temp_l in zip(_FAN_SPEEDS, _TEMP_THRS_L) if temp_l > max_temp) if new_speed_h > fan_speed: # update speed if using the high thresholds results in fan speed increment fan_speed = new_speed_h elif new_speed_l < fan_speed: # update speed if using the low thresholds results in fan speed decrement fan_speed = new_speed_l if bat_temp < _BAT_TEMP_THERSHOLD: # no max fan speed unless battery is hot fan_speed = min(fan_speed, _FAN_SPEEDS[-2]) set_eon_fan(fan_speed/16384) return fan_speed class LocationStarter(object): def __init__(self): self.last_good_loc = 0 def update(self, started_ts, location): rt = sec_since_boot() if location is None or location.accuracy > 50 or location.speed < 2: # bad location, stop if we havent gotten a location in a while # dont stop if we're been going for less than a minute if started_ts: if rt-self.last_good_loc > 60. and rt-started_ts > 60: cloudlog.event("location_stop", ts=rt, started_ts=started_ts, last_good_loc=self.last_good_loc, location=location.to_dict() if location else None) return False else: return True else: return False self.last_good_loc = rt if started_ts: return True else: cloudlog.event("location_start", location=location.to_dict() if location else None) return location.speed*3.6 > 10 def manager_thread(): # now loop context = zmq.Context() thermal_sock = messaging.pub_sock(context, service_list['thermal'].port) health_sock = messaging.sub_sock(context, service_list['health'].port) location_sock = messaging.sub_sock(context, service_list['gpsLocation'].port) cloudlog.info("manager start") cloudlog.info({"environ": os.environ}) for p in persistent_processes: start_managed_process(p) # start frame system("am start -n ai.comma.plus.frame/.MainActivity") # do this before panda flashing setup_eon_fan() if os.getenv("NOBOARD") is None: start_managed_process("pandad") params = Params() passive_starter = LocationStarter() started_ts = None off_ts = None logger_dead = False count = 0 fan_speed = 0 ignition_seen = False started_seen = False panda_seen = False health_sock.RCVTIMEO = 1500 while 1: # get health of board, log this in "thermal" td = messaging.recv_sock(health_sock, wait=True) location = messaging.recv_sock(location_sock) location = location.gpsLocation if location else None print td # replace thermald msg = read_thermal() # loggerd is gated based on free space statvfs = os.statvfs(ROOT) avail = (statvfs.f_bavail * 1.0)/statvfs.f_blocks # thermal message now also includes free space msg.thermal.freeSpace = avail with open("/sys/class/power_supply/battery/capacity") as f: msg.thermal.batteryPercent = int(f.read()) with open("/sys/class/power_supply/battery/status") as f: msg.thermal.batteryStatus = f.read().strip() with open("/sys/class/power_supply/usb/online") as f: msg.thermal.usbOnline = bool(int(f.read())) # TODO: add car battery voltage check max_temp = max(msg.thermal.cpu0, msg.thermal.cpu1, msg.thermal.cpu2, msg.thermal.cpu3) / 10.0 bat_temp = msg.thermal.bat/1000. fan_speed = handle_fan(max_temp, bat_temp, fan_speed) msg.thermal.fanSpeed = fan_speed msg.thermal.started = started_ts is not None msg.thermal.startedTs = int(1e9*(started_ts or 0)) thermal_sock.send(msg.to_bytes()) print msg # uploader is gated based on the phone temperature if max_temp > 85.0: cloudlog.warning("over temp: %r", max_temp) kill_managed_process("uploader") elif max_temp < 70.0: start_managed_process("uploader") if avail < 0.05: logger_dead = True # start constellation of processes when the car starts ignition = td is not None and td.health.started ignition_seen = ignition_seen or ignition # add voltage check for ignition if not ignition_seen and td is not None and td.health.voltage > 13500: ignition = True do_uninstall = params.get("DoUninstall") == "1" accepted_terms = params.get("HasAcceptedTerms") == "1" completed_training = params.get("CompletedTrainingVersion") == training_version should_start = ignition # have we seen a panda? panda_seen = panda_seen or td is not None # start on gps movement if we haven't seen ignition and are in passive mode should_start = should_start or (not (ignition_seen and td) # seen ignition and panda is connected and params.get("Passive") == "1" and passive_starter.update(started_ts, location)) # with 2% left, we killall, otherwise the phone will take a long time to boot should_start = should_start and avail > 0.02 # require usb power should_start = should_start and msg.thermal.usbOnline should_start = should_start and accepted_terms and completed_training and (not do_uninstall) # if any CPU gets above 107 or the battery gets above 63, kill all processes # controls will warn with CPU above 95 or battery above 60 if max_temp > 107.0 or msg.thermal.bat >= 63000: # TODO: Add a better warning when this is happening should_start = False if should_start: off_ts = None if started_ts is None: params.car_start() started_ts = sec_since_boot() started_seen = True for p in car_started_processes: if p == "loggerd" and logger_dead: kill_managed_process(p) else: start_managed_process(p) else: started_ts = None if off_ts is None: off_ts = sec_since_boot() logger_dead = False for p in car_started_processes: kill_managed_process(p) # shutdown if the battery gets lower than 3%, t's discharging, we aren't running for # more than a minute but we were running if msg.thermal.batteryPercent < 3 and msg.thermal.batteryStatus == "Discharging" and \ started_seen and (sec_since_boot() - off_ts) > 60: os.system('LD_LIBRARY_PATH="" svc power shutdown') # check the status of all processes, did any of them die? for p in running: cloudlog.debug(" running %s %s" % (p, running[p])) # report to server once per minute if (count%60) == 0: cloudlog.event("STATUS_PACKET", running=running.keys(), count=count, health=(td.to_dict() if td else None), location=(location.to_dict() if location else None), thermal=msg.to_dict()) if do_uninstall: break count += 1 def get_installed_apks(): dat = subprocess.check_output(["pm", "list", "packages", "-f"]).strip().split("\n") ret = {} for x in dat: if x.startswith("package:"): v,k = x.split("package:")[1].split("=") ret[k] = v return ret def install_apk(path): # can only install from world readable path install_path = "/sdcard/%s" % os.path.basename(path) shutil.copyfile(path, install_path) ret = subprocess.call(["pm", "install", "-r", install_path]) os.remove(install_path) return ret == 0 def update_apks(): # patch apks if os.getenv("PREPAREONLY"): # assume we have internet, download too patched = subprocess.call([os.path.join(BASEDIR, "apk/external/patcher.py")]) else: patched = subprocess.call([os.path.join(BASEDIR, "apk/external/patcher.py"), "patch"]) cloudlog.info("patcher: %r" % (patched,)) # install apks installed = get_installed_apks() install_apks = (glob.glob(os.path.join(BASEDIR, "apk/*.apk")) + glob.glob(os.path.join(BASEDIR, "apk/external/out/*.apk"))) for apk in install_apks: app = os.path.basename(apk)[:-4] if app not in installed: installed[app] = None cloudlog.info("installed apks %s" % (str(installed), )) for app in installed.iterkeys(): apk_path = os.path.join(BASEDIR, "apk/"+app+".apk") if not os.path.exists(apk_path): apk_path = os.path.join(BASEDIR, "apk/external/out/"+app+".apk") if not os.path.exists(apk_path): continue h1 = hashlib.sha1(open(apk_path).read()).hexdigest() h2 = None if installed[app] is not None: h2 = hashlib.sha1(open(installed[app]).read()).hexdigest() cloudlog.info("comparing version of %s %s vs %s" % (app, h1, h2)) if h2 is None or h1 != h2: cloudlog.info("installing %s" % app) success = install_apk(apk_path) if not success: cloudlog.info("needing to uninstall %s" % app) system("pm uninstall %s" % app) success = install_apk(apk_path) assert success def manager_update(): update_apks() def manager_prepare(): # build cereal first subprocess.check_call(["make", "-j4"], cwd=os.path.join(BASEDIR, "cereal")) # build all processes os.chdir(os.path.dirname(os.path.abspath(__file__))) for p in managed_processes: prepare_managed_process(p) def uninstall(): cloudlog.warning("uninstalling") with open('/cache/recovery/command', 'w') as f: f.write('--wipe_data\n') # IPowerManager.reboot(confirm=false, reason="recovery", wait=true) os.system("service call power 16 i32 0 s16 recovery i32 1") def main(): # the flippening! os.system('LD_LIBRARY_PATH="" content insert --uri content://settings/system --bind name:s:user_rotation --bind value:i:1') if os.getenv("NOLOG") is not None: del managed_processes['loggerd'] del managed_processes['tombstoned'] if os.getenv("NOUPLOAD") is not None: del managed_processes['uploader'] if os.getenv("NOVISION") is not None: del managed_processes['visiond'] if os.getenv("LEAN") is not None: del managed_processes['uploader'] del managed_processes['loggerd'] del managed_processes['logmessaged'] del managed_processes['logcatd'] del managed_processes['tombstoned'] del managed_processes['proclogd'] if os.getenv("NOCONTROL") is not None: del managed_processes['controlsd'] del managed_processes['radard'] # support additional internal only extensions try: import selfdrive.manager_extensions selfdrive.manager_extensions.register(register_managed_process) except ImportError: pass params = Params() params.manager_start() # set unset params if params.get("IsMetric") is None: params.put("IsMetric", "0") if params.get("RecordFront") is None: params.put("RecordFront", "0") if params.get("IsRearViewMirror") is None: params.put("IsRearViewMirror", "0") if params.get("IsFcwEnabled") is None: params.put("IsFcwEnabled", "1") if params.get("HasAcceptedTerms") is None: params.put("HasAcceptedTerms", "0") if params.get("IsUploadVideoOverCellularEnabled") is None: params.put("IsUploadVideoOverCellularEnabled", "1") # is this chffrplus? if os.getenv("PASSIVE") is not None: params.put("Passive", str(int(os.getenv("PASSIVE")))) if params.get("Passive") is None: raise Exception("Passive must be set to continue") # put something on screen while we set things up if os.getenv("PREPAREONLY") is not None: spinner_proc = None else: spinner_proc = subprocess.Popen(["./spinner", "loading..."], cwd=os.path.join(BASEDIR, "selfdrive", "ui", "spinner"), close_fds=True) try: manager_update() manager_init() manager_prepare() finally: if spinner_proc: spinner_proc.terminate() 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() crash.capture_exception() finally: cleanup_all_processes(None, None) if params.get("DoUninstall") == "1": uninstall() if __name__ == "__main__": main() # manual exit because we are forked sys.exit(0)