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.
280 lines
8.1 KiB
280 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):
|
|
'''Raises an exception in the threads with id tid'''
|
|
for ctid, tobj in threading._active.items():
|
|
if tobj is t:
|
|
tid = ctid
|
|
break
|
|
else:
|
|
raise Exception("Could not find thread")
|
|
|
|
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()
|
|
|