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.
		
		
		
		
		
			
		
			
				
					
					
						
							278 lines
						
					
					
						
							8.1 KiB
						
					
					
				
			
		
		
	
	
							278 lines
						
					
					
						
							8.1 KiB
						
					
					
				| #!/usr/bin/env python3
 | |
| import os
 | |
| import re
 | |
| import time
 | |
| import json
 | |
| import random
 | |
| import ctypes
 | |
| import inspect
 | |
| import requests
 | |
| import traceback
 | |
| import threading
 | |
| import subprocess
 | |
| 
 | |
| from selfdrive.swaglog import cloudlog
 | |
| from selfdrive.loggerd.config import ROOT
 | |
| 
 | |
| from common import android
 | |
| from common.params import Params
 | |
| from common.api import Api
 | |
| from common.xattr import getxattr, setxattr
 | |
| 
 | |
| UPLOAD_ATTR_NAME = 'user.upload'
 | |
| UPLOAD_ATTR_VALUE = b'1'
 | |
| 
 | |
| fake_upload = os.getenv("FAKEUPLOAD") is not None
 | |
| 
 | |
| def raise_on_thread(t, exctype):
 | |
|   for ctid, tobj in threading._active.items():
 | |
|     if tobj is t:
 | |
|       tid = ctid
 | |
|       break
 | |
|   else:
 | |
|     raise Exception("Could not find thread")
 | |
| 
 | |
|   '''Raises an exception in the threads with id tid'''
 | |
|   if not inspect.isclass(exctype):
 | |
|     raise TypeError("Only types can be raised (not instances)")
 | |
| 
 | |
|   res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid),
 | |
|                                                    ctypes.py_object(exctype))
 | |
|   if res == 0:
 | |
|     raise ValueError("invalid thread id")
 | |
|   elif res != 1:
 | |
|     # "if it returns a number greater than one, you're in trouble,
 | |
|     # and you should call it again with exc=NULL to revert the effect"
 | |
|     ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0)
 | |
|     raise SystemError("PyThreadState_SetAsyncExc failed")
 | |
| 
 | |
| def get_directory_sort(d):
 | |
|   return list(map(lambda s: s.rjust(10, '0'), d.rsplit('--', 1)))
 | |
| 
 | |
| def listdir_by_creation(d):
 | |
|   try:
 | |
|     paths = os.listdir(d)
 | |
|     paths = sorted(paths, key=get_directory_sort)
 | |
|     return paths
 | |
|   except OSError:
 | |
|     cloudlog.exception("listdir_by_creation failed")
 | |
|     return list()
 | |
| 
 | |
| def clear_locks(root):
 | |
|   for logname in os.listdir(root):
 | |
|     path = os.path.join(root, logname)
 | |
|     try:
 | |
|       for fname in os.listdir(path):
 | |
|         if fname.endswith(".lock"):
 | |
|           os.unlink(os.path.join(path, fname))
 | |
|     except OSError:
 | |
|       cloudlog.exception("clear_locks failed")
 | |
| 
 | |
| def is_on_wifi():
 | |
|   # ConnectivityManager.getActiveNetworkInfo()
 | |
|   try:
 | |
|     # TODO: figure out why the android service call sometimes dies with SIGUSR2 (signal from MSGQ)
 | |
|     result = android.parse_service_call_string(android.service_call(["connectivity", "2"]))
 | |
|     if result is None:
 | |
|       return True
 | |
|     return 'WIFI' in result
 | |
|   except Exception:
 | |
|     cloudlog.exception("is_on_wifi failed")
 | |
|     return False
 | |
| 
 | |
| def is_on_hotspot():
 | |
|   try:
 | |
|     result = subprocess.check_output(["ifconfig", "wlan0"], stderr=subprocess.STDOUT, encoding='utf8')
 | |
|     result = re.findall(r"inet addr:((\d+\.){3}\d+)", result)[0][0]
 | |
| 
 | |
|     is_android = result.startswith('192.168.43.')
 | |
|     is_ios = result.startswith('172.20.10.')
 | |
|     is_entune = result.startswith('10.0.2.')
 | |
| 
 | |
|     return (is_android or is_ios or is_entune)
 | |
|   except Exception:
 | |
|     return False
 | |
| 
 | |
| class Uploader():
 | |
|   def __init__(self, dongle_id, root):
 | |
|     self.dongle_id = dongle_id
 | |
|     self.api = Api(dongle_id)
 | |
|     self.root = root
 | |
| 
 | |
|     self.upload_thread = None
 | |
| 
 | |
|     self.last_resp = None
 | |
|     self.last_exc = None
 | |
| 
 | |
|     self.immediate_priority = {"qlog.bz2": 0, "qcamera.ts": 1}
 | |
|     self.high_priority = {"rlog.bz2": 0, "fcamera.hevc": 1, "dcamera.hevc": 2}
 | |
| 
 | |
|   def get_upload_sort(self, name):
 | |
|     if name in self.immediate_priority:
 | |
|       return self.immediate_priority[name]
 | |
|     if name in self.high_priority:
 | |
|       return self.high_priority[name] + 100
 | |
|     return 1000
 | |
| 
 | |
|   def gen_upload_files(self):
 | |
|     if not os.path.isdir(self.root):
 | |
|       return
 | |
|     for logname in listdir_by_creation(self.root):
 | |
|       path = os.path.join(self.root, logname)
 | |
|       try:
 | |
|         names = os.listdir(path)
 | |
|       except OSError:
 | |
|         continue
 | |
|       if any(name.endswith(".lock") for name in names):
 | |
|         continue
 | |
| 
 | |
|       for name in sorted(names, key=self.get_upload_sort):
 | |
|         key = os.path.join(logname, name)
 | |
|         fn = os.path.join(path, name)
 | |
|         # skip files already uploaded
 | |
|         try:
 | |
|           is_uploaded = getxattr(fn, UPLOAD_ATTR_NAME)
 | |
|         except OSError:
 | |
|           cloudlog.event("uploader_getxattr_failed", exc=self.last_exc, key=key, fn=fn)
 | |
|           is_uploaded = True # deleter could have deleted
 | |
|         if is_uploaded:
 | |
|           continue
 | |
| 
 | |
|         yield (name, key, fn)
 | |
| 
 | |
|   def next_file_to_upload(self, with_raw):
 | |
|     upload_files = list(self.gen_upload_files())
 | |
|     # try to upload qlog files first
 | |
|     for name, key, fn in upload_files:
 | |
|       if name in self.immediate_priority:
 | |
|         return (key, fn)
 | |
| 
 | |
|     if with_raw:
 | |
|       # then upload the full log files, rear and front camera files
 | |
|       for name, key, fn in upload_files:
 | |
|         if name in self.high_priority:
 | |
|           return (key, fn)
 | |
| 
 | |
|       # then upload other files
 | |
|       for name, key, fn in upload_files:
 | |
|         if not name.endswith('.lock') and not name.endswith(".tmp"):
 | |
|           return (key, fn)
 | |
| 
 | |
|     return None
 | |
| 
 | |
|   def do_upload(self, key, fn):
 | |
|     try:
 | |
|       url_resp = self.api.get("v1.3/"+self.dongle_id+"/upload_url/", timeout=10, path=key, access_token=self.api.get_token())
 | |
|       if url_resp.status_code == 412:
 | |
|         self.last_resp = url_resp
 | |
|         return
 | |
|         
 | |
|       url_resp_json = json.loads(url_resp.text)
 | |
|       url = url_resp_json['url']
 | |
|       headers = url_resp_json['headers']
 | |
|       cloudlog.info("upload_url v1.3 %s %s", url, str(headers))
 | |
| 
 | |
|       if fake_upload:
 | |
|         cloudlog.info("*** WARNING, THIS IS A FAKE UPLOAD TO %s ***" % url)
 | |
|         class FakeResponse():
 | |
|           def __init__(self):
 | |
|             self.status_code = 200
 | |
|         self.last_resp = FakeResponse()
 | |
|       else:
 | |
|         with open(fn, "rb") as f:
 | |
|           self.last_resp = requests.put(url, data=f, headers=headers, timeout=10)
 | |
|     except Exception as e:
 | |
|       self.last_exc = (e, traceback.format_exc())
 | |
|       raise
 | |
| 
 | |
|   def normal_upload(self, key, fn):
 | |
|     self.last_resp = None
 | |
|     self.last_exc = None
 | |
| 
 | |
|     try:
 | |
|       self.do_upload(key, fn)
 | |
|     except Exception:
 | |
|       pass
 | |
| 
 | |
|     return self.last_resp
 | |
| 
 | |
|   def upload(self, key, fn):
 | |
|     try:
 | |
|       sz = os.path.getsize(fn)
 | |
|     except OSError:
 | |
|       cloudlog.exception("upload: getsize failed")
 | |
|       return False
 | |
| 
 | |
|     cloudlog.event("upload", key=key, fn=fn, sz=sz)
 | |
| 
 | |
|     cloudlog.info("checking %r with size %r", key, sz)
 | |
| 
 | |
|     if sz == 0:
 | |
|       try:
 | |
|         # tag files of 0 size as uploaded
 | |
|         setxattr(fn, UPLOAD_ATTR_NAME, UPLOAD_ATTR_VALUE)
 | |
|       except OSError:
 | |
|         cloudlog.event("uploader_setxattr_failed", exc=self.last_exc, key=key, fn=fn, sz=sz)
 | |
|       success = True
 | |
|     else:
 | |
|       cloudlog.info("uploading %r", fn)
 | |
|       stat = self.normal_upload(key, fn)
 | |
|       if stat is not None and stat.status_code in (200, 201, 412):
 | |
|         cloudlog.event("upload_success" if stat.status_code != 412 else "upload_ignored", key=key, fn=fn, sz=sz)
 | |
|         try:
 | |
|           # tag file as uploaded
 | |
|           setxattr(fn, UPLOAD_ATTR_NAME, UPLOAD_ATTR_VALUE)
 | |
|         except OSError:
 | |
|           cloudlog.event("uploader_setxattr_failed", exc=self.last_exc, key=key, fn=fn, sz=sz)
 | |
|         success = True
 | |
|       else:
 | |
|         cloudlog.event("upload_failed", stat=stat, exc=self.last_exc, key=key, fn=fn, sz=sz)
 | |
|         success = False
 | |
| 
 | |
|     return success
 | |
| 
 | |
| def uploader_fn(exit_event):
 | |
|   cloudlog.info("uploader_fn")
 | |
| 
 | |
|   params = Params()
 | |
|   dongle_id = params.get("DongleId").decode('utf8')
 | |
| 
 | |
|   if dongle_id is None:
 | |
|     cloudlog.info("uploader missing dongle_id")
 | |
|     raise Exception("uploader can't start without dongle id")
 | |
| 
 | |
|   uploader = Uploader(dongle_id, ROOT)
 | |
| 
 | |
|   backoff = 0.1
 | |
|   while True:
 | |
|     allow_raw_upload = (params.get("IsUploadRawEnabled") != b"0")
 | |
|     on_hotspot = is_on_hotspot()
 | |
|     on_wifi = is_on_wifi()
 | |
|     should_upload = on_wifi and not on_hotspot
 | |
| 
 | |
|     if exit_event.is_set():
 | |
|       return
 | |
| 
 | |
|     d = uploader.next_file_to_upload(with_raw=allow_raw_upload and should_upload)
 | |
|     if d is None:
 | |
|       time.sleep(5)
 | |
|       continue
 | |
| 
 | |
|     key, fn = d
 | |
| 
 | |
|     cloudlog.event("uploader_netcheck", is_on_hotspot=on_hotspot, is_on_wifi=on_wifi)
 | |
|     cloudlog.info("to upload %r", d)
 | |
|     success = uploader.upload(key, fn)
 | |
|     if success:
 | |
|       backoff = 0.1
 | |
|     else:
 | |
|       cloudlog.info("backoff %r", backoff)
 | |
|       time.sleep(backoff + random.uniform(0, backoff))
 | |
|       backoff = min(backoff*2, 120)
 | |
|     cloudlog.info("upload done, success=%r", success)
 | |
| 
 | |
| def main():
 | |
|   uploader_fn(threading.Event())
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|   main()
 | |
| 
 |