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.
288 lines
8.1 KiB
288 lines
8.1 KiB
5 years ago
|
#!/usr/bin/env python3
|
||
3 years ago
|
import bz2
|
||
3 years ago
|
import io
|
||
5 years ago
|
import json
|
||
5 years ago
|
import os
|
||
5 years ago
|
import random
|
||
5 years ago
|
import requests
|
||
5 years ago
|
import threading
|
||
5 years ago
|
import time
|
||
|
import traceback
|
||
4 years ago
|
from pathlib import Path
|
||
5 years ago
|
|
||
5 years ago
|
from cereal import log
|
||
4 years ago
|
import cereal.messaging as messaging
|
||
5 years ago
|
from common.api import Api
|
||
5 years ago
|
from common.params import Params
|
||
3 years ago
|
from common.realtime import set_core_affinity
|
||
3 years ago
|
from system.hardware import TICI
|
||
2 years ago
|
from system.loggerd.xattr_cache import getxattr, setxattr
|
||
|
from system.loggerd.config import ROOT
|
||
3 years ago
|
from system.swaglog import cloudlog
|
||
5 years ago
|
|
||
4 years ago
|
NetworkType = log.DeviceState.NetworkType
|
||
5 years ago
|
UPLOAD_ATTR_NAME = 'user.upload'
|
||
|
UPLOAD_ATTR_VALUE = b'1'
|
||
5 years ago
|
|
||
3 years ago
|
UPLOAD_QLOG_QCAM_MAX_SIZE = 100 * 1e6 # MB
|
||
3 years ago
|
|
||
4 years ago
|
allow_sleep = bool(os.getenv("UPLOADER_SLEEP", "1"))
|
||
4 years ago
|
force_wifi = os.getenv("FORCEWIFI") is not None
|
||
5 years ago
|
fake_upload = os.getenv("FAKEUPLOAD") is not None
|
||
|
|
||
5 years ago
|
|
||
5 years ago
|
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")
|
||
|
|
||
|
|
||
|
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
|
||
|
|
||
4 years ago
|
self.immediate_size = 0
|
||
|
self.immediate_count = 0
|
||
|
|
||
|
# stats for last successfully uploaded file
|
||
3 years ago
|
self.last_time = 0.0
|
||
|
self.last_speed = 0.0
|
||
4 years ago
|
self.last_filename = ""
|
||
|
|
||
4 years ago
|
self.immediate_folders = ["crash/", "boot/"]
|
||
3 years ago
|
self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1}
|
||
5 years ago
|
|
||
|
def get_upload_sort(self, name):
|
||
|
if name in self.immediate_priority:
|
||
|
return self.immediate_priority[name]
|
||
|
return 1000
|
||
|
|
||
4 years ago
|
def list_upload_files(self):
|
||
5 years ago
|
if not os.path.isdir(self.root):
|
||
|
return
|
||
4 years ago
|
|
||
|
self.immediate_size = 0
|
||
|
self.immediate_count = 0
|
||
|
|
||
5 years ago
|
for logname in listdir_by_creation(self.root):
|
||
|
path = os.path.join(self.root, logname)
|
||
|
try:
|
||
|
names = os.listdir(path)
|
||
|
except OSError:
|
||
|
continue
|
||
4 years ago
|
|
||
5 years ago
|
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)
|
||
5 years ago
|
# 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)
|
||
5 years ago
|
is_uploaded = True # deleter could have deleted
|
||
5 years ago
|
if is_uploaded:
|
||
|
continue
|
||
4 years ago
|
|
||
|
try:
|
||
|
if name in self.immediate_priority:
|
||
|
self.immediate_count += 1
|
||
|
self.immediate_size += os.path.getsize(fn)
|
||
|
except OSError:
|
||
|
pass
|
||
|
|
||
5 years ago
|
yield (name, key, fn)
|
||
|
|
||
3 years ago
|
def next_file_to_upload(self):
|
||
4 years ago
|
upload_files = list(self.list_upload_files())
|
||
5 years ago
|
|
||
5 years ago
|
for name, key, fn in upload_files:
|
||
3 years ago
|
if any(f in fn for f in self.immediate_folders):
|
||
3 years ago
|
return (name, key, fn)
|
||
5 years ago
|
|
||
3 years ago
|
for name, key, fn in upload_files:
|
||
|
if name in self.immediate_priority:
|
||
3 years ago
|
return (name, key, fn)
|
||
5 years ago
|
|
||
|
return None
|
||
|
|
||
|
def do_upload(self, key, fn):
|
||
|
try:
|
||
3 years ago
|
url_resp = self.api.get("v1.4/" + self.dongle_id + "/upload_url/", timeout=10, path=key, access_token=self.api.get_token())
|
||
5 years ago
|
if url_resp.status_code == 412:
|
||
|
self.last_resp = url_resp
|
||
|
return
|
||
5 years ago
|
|
||
5 years ago
|
url_resp_json = json.loads(url_resp.text)
|
||
|
url = url_resp_json['url']
|
||
|
headers = url_resp_json['headers']
|
||
3 years ago
|
cloudlog.debug("upload_url v1.4 %s %s", url, str(headers))
|
||
5 years ago
|
|
||
|
if fake_upload:
|
||
3 years ago
|
cloudlog.debug(f"*** WARNING, THIS IS A FAKE UPLOAD TO {url} ***")
|
||
5 years ago
|
|
||
5 years ago
|
class FakeResponse():
|
||
|
def __init__(self):
|
||
|
self.status_code = 200
|
||
5 years ago
|
|
||
5 years ago
|
self.last_resp = FakeResponse()
|
||
|
else:
|
||
|
with open(fn, "rb") as f:
|
||
3 years ago
|
if key.endswith('.bz2') and not fn.endswith('.bz2'):
|
||
3 years ago
|
data = bz2.compress(f.read())
|
||
|
data = io.BytesIO(data)
|
||
|
else:
|
||
|
data = f
|
||
3 years ago
|
|
||
|
self.last_resp = requests.put(url, data=data, headers=headers, timeout=10)
|
||
5 years ago
|
except Exception as e:
|
||
5 years ago
|
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)
|
||
5 years ago
|
except Exception:
|
||
5 years ago
|
pass
|
||
|
|
||
|
return self.last_resp
|
||
|
|
||
3 years ago
|
def upload(self, name, key, fn, network_type, metered):
|
||
5 years ago
|
try:
|
||
|
sz = os.path.getsize(fn)
|
||
|
except OSError:
|
||
|
cloudlog.exception("upload: getsize failed")
|
||
|
return False
|
||
|
|
||
3 years ago
|
cloudlog.event("upload_start", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)
|
||
5 years ago
|
|
||
|
if sz == 0:
|
||
3 years ago
|
# tag files of 0 size as uploaded
|
||
|
success = True
|
||
|
elif name in self.immediate_priority and sz > UPLOAD_QLOG_QCAM_MAX_SIZE:
|
||
|
cloudlog.event("uploader_too_large", key=key, fn=fn, sz=sz)
|
||
5 years ago
|
success = True
|
||
|
else:
|
||
4 years ago
|
start_time = time.monotonic()
|
||
5 years ago
|
stat = self.normal_upload(key, fn)
|
||
3 years ago
|
if stat is not None and stat.status_code in (200, 201, 401, 403, 412):
|
||
4 years ago
|
self.last_filename = fn
|
||
|
self.last_time = time.monotonic() - start_time
|
||
|
self.last_speed = (sz / 1e6) / self.last_time
|
||
5 years ago
|
success = True
|
||
3 years ago
|
cloudlog.event("upload_success" if stat.status_code != 412 else "upload_ignored", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)
|
||
5 years ago
|
else:
|
||
|
success = False
|
||
3 years ago
|
cloudlog.event("upload_failed", stat=stat, exc=self.last_exc, key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)
|
||
5 years ago
|
|
||
3 years ago
|
if success:
|
||
|
# tag file as uploaded
|
||
|
try:
|
||
|
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)
|
||
|
|
||
5 years ago
|
return success
|
||
|
|
||
4 years ago
|
def get_msg(self):
|
||
|
msg = messaging.new_message("uploaderState")
|
||
|
us = msg.uploaderState
|
||
|
us.immediateQueueSize = int(self.immediate_size / 1e6)
|
||
|
us.immediateQueueCount = self.immediate_count
|
||
|
us.lastTime = self.last_time
|
||
|
us.lastSpeed = self.last_speed
|
||
|
us.lastFilename = self.last_filename
|
||
|
return msg
|
||
|
|
||
3 years ago
|
|
||
5 years ago
|
def uploader_fn(exit_event):
|
||
3 years ago
|
try:
|
||
|
set_core_affinity([0, 1, 2, 3])
|
||
|
except Exception:
|
||
|
cloudlog.exception("failed to set core affinity")
|
||
|
|
||
3 years ago
|
clear_locks(ROOT)
|
||
|
|
||
5 years ago
|
params = Params()
|
||
4 years ago
|
dongle_id = params.get("DongleId", encoding='utf8')
|
||
5 years ago
|
|
||
|
if dongle_id is None:
|
||
|
cloudlog.info("uploader missing dongle_id")
|
||
|
raise Exception("uploader can't start without dongle id")
|
||
|
|
||
4 years ago
|
if TICI and not Path("/data/media").is_mount():
|
||
4 years ago
|
cloudlog.warning("NVME not mounted")
|
||
4 years ago
|
|
||
4 years ago
|
sm = messaging.SubMaster(['deviceState'])
|
||
4 years ago
|
pm = messaging.PubMaster(['uploaderState'])
|
||
5 years ago
|
uploader = Uploader(dongle_id, ROOT)
|
||
|
|
||
|
backoff = 0.1
|
||
5 years ago
|
while not exit_event.is_set():
|
||
4 years ago
|
sm.update(0)
|
||
4 years ago
|
offroad = params.get_bool("IsOffroad")
|
||
4 years ago
|
network_type = sm['deviceState'].networkType if not force_wifi else NetworkType.wifi
|
||
|
if network_type == NetworkType.none:
|
||
|
if allow_sleep:
|
||
|
time.sleep(60 if offroad else 5)
|
||
|
continue
|
||
|
|
||
3 years ago
|
d = uploader.next_file_to_upload()
|
||
5 years ago
|
if d is None: # Nothing to upload
|
||
4 years ago
|
if allow_sleep:
|
||
|
time.sleep(60 if offroad else 5)
|
||
5 years ago
|
continue
|
||
|
|
||
3 years ago
|
name, key, fn = d
|
||
5 years ago
|
|
||
3 years ago
|
# qlogs and bootlogs need to be compressed before uploading
|
||
3 years ago
|
if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.bz2')):
|
||
3 years ago
|
key += ".bz2"
|
||
|
|
||
3 years ago
|
success = uploader.upload(name, key, fn, sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered)
|
||
5 years ago
|
if success:
|
||
|
backoff = 0.1
|
||
4 years ago
|
elif allow_sleep:
|
||
4 years ago
|
cloudlog.info("upload backoff %r", backoff)
|
||
5 years ago
|
time.sleep(backoff + random.uniform(0, backoff))
|
||
|
backoff = min(backoff*2, 120)
|
||
4 years ago
|
|
||
|
pm.send("uploaderState", uploader.get_msg())
|
||
5 years ago
|
|
||
3 years ago
|
|
||
5 years ago
|
def main():
|
||
5 years ago
|
uploader_fn(threading.Event())
|
||
|
|
||
5 years ago
|
|
||
5 years ago
|
if __name__ == "__main__":
|
||
|
main()
|