|  |  | @ -1,4 +1,6 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  | #!/usr/bin/env python3 |  |  |  | #!/usr/bin/env python3 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | from __future__ import annotations | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | import base64 |  |  |  | import base64 | 
			
		
	
		
		
			
				
					
					|  |  |  | import bz2 |  |  |  | import bz2 | 
			
		
	
		
		
			
				
					
					|  |  |  | import hashlib |  |  |  | import hashlib | 
			
		
	
	
		
		
			
				
					|  |  | @ -14,14 +16,15 @@ import sys | 
			
		
	
		
		
			
				
					
					|  |  |  | import tempfile |  |  |  | import tempfile | 
			
		
	
		
		
			
				
					
					|  |  |  | import threading |  |  |  | import threading | 
			
		
	
		
		
			
				
					
					|  |  |  | import time |  |  |  | import time | 
			
		
	
		
		
			
				
					
					|  |  |  | from collections import namedtuple |  |  |  | from dataclasses import asdict, dataclass, replace | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | from datetime import datetime |  |  |  | from datetime import datetime | 
			
		
	
		
		
			
				
					
					|  |  |  | from functools import partial |  |  |  | from functools import partial | 
			
		
	
		
		
			
				
					
					|  |  |  | from typing import Any, Dict |  |  |  | from queue import Queue | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | from typing import BinaryIO, Callable, Dict, List, Optional, Set, Union, cast | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | import requests |  |  |  | import requests | 
			
		
	
		
		
			
				
					
					|  |  |  | from jsonrpc import JSONRPCResponseManager, dispatcher |  |  |  | from jsonrpc import JSONRPCResponseManager, dispatcher | 
			
		
	
		
		
			
				
					
					|  |  |  | from websocket import (ABNF, WebSocketException, WebSocketTimeoutException, |  |  |  | from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |                        create_connection) |  |  |  |                        create_connection) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | import cereal.messaging as messaging |  |  |  | import cereal.messaging as messaging | 
			
		
	
	
		
		
			
				
					|  |  | @ -54,19 +57,54 @@ WS_FRAME_SIZE = 4096 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | NetworkType = log.DeviceState.NetworkType |  |  |  | NetworkType = log.DeviceState.NetworkType | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | UploadFileDict = Dict[str, Union[str, int, float, bool]] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | UploadItemDict = Dict[str, Union[str, bool, int, float, Dict[str, str]]] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | UploadFilesToUrlResponse = Dict[str, Union[int, List[UploadItemDict], List[str]]] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | @dataclass | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | class UploadFile: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   fn: str | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   url: str | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   headers: Dict[str, str] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   allow_cellular: bool | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   @classmethod | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   def from_dict(cls, d: Dict) -> UploadFile: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     return cls(d.get("fn", ""), d.get("url", ""), d.get("headers", {}), d.get("allow_cellular", False)) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | @dataclass | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | class UploadItem: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   path: str | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   url: str | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   headers: Dict[str, str] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   created_at: int | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   id: Optional[str] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   retry_count: int = 0 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   current: bool = False | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   progress: float = 0 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   allow_cellular: bool = False | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   @classmethod | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   def from_dict(cls, d: Dict) -> UploadItem: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     return cls(d["path"], d["url"], d["headers"], d["created_at"], d["id"], d["retry_count"], d["current"], | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                d["progress"], d["allow_cellular"]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | dispatcher["echo"] = lambda s: s |  |  |  | dispatcher["echo"] = lambda s: s | 
			
		
	
		
		
			
				
					
					|  |  |  | recv_queue: Any = queue.Queue() |  |  |  | recv_queue: Queue[str] = queue.Queue() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | send_queue: Any = queue.Queue() |  |  |  | send_queue: Queue[str] = queue.Queue() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | upload_queue: Any = queue.Queue() |  |  |  | upload_queue: Queue[UploadItem] = queue.Queue() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | low_priority_send_queue: Any = queue.Queue() |  |  |  | low_priority_send_queue: Queue[str] = queue.Queue() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | log_recv_queue: Any = queue.Queue() |  |  |  | log_recv_queue: Queue[str] = queue.Queue() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | cancelled_uploads: Any = set() |  |  |  | cancelled_uploads: Set[str] = set() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | UploadItem = namedtuple('UploadItem', ['path', 'url', 'headers', 'created_at', 'id', 'retry_count', 'current', 'progress', 'allow_cellular'], defaults=(0, False, 0, False)) |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | cur_upload_items: Dict[int, Any] = {} |  |  |  | cur_upload_items: Dict[int, Optional[UploadItem]] = {} | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def strip_bz2_extension(fn): |  |  |  | def strip_bz2_extension(fn: str) -> str: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if fn.endswith('.bz2'): |  |  |  |   if fn.endswith('.bz2'): | 
			
		
	
		
		
			
				
					
					|  |  |  |     return fn[:-4] |  |  |  |     return fn[:-4] | 
			
		
	
		
		
			
				
					
					|  |  |  |   return fn |  |  |  |   return fn | 
			
		
	
	
		
		
			
				
					|  |  | @ -76,29 +114,30 @@ class AbortTransferException(Exception): | 
			
		
	
		
		
			
				
					
					|  |  |  |   pass |  |  |  |   pass | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | class UploadQueueCache(): |  |  |  | class UploadQueueCache: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   params = Params() |  |  |  |   params = Params() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   @staticmethod |  |  |  |   @staticmethod | 
			
		
	
		
		
			
				
					
					|  |  |  |   def initialize(upload_queue): |  |  |  |   def initialize(upload_queue: Queue[UploadItem]) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       upload_queue_json = UploadQueueCache.params.get("AthenadUploadQueue") |  |  |  |       upload_queue_json = UploadQueueCache.params.get("AthenadUploadQueue") | 
			
		
	
		
		
			
				
					
					|  |  |  |       if upload_queue_json is not None: |  |  |  |       if upload_queue_json is not None: | 
			
		
	
		
		
			
				
					
					|  |  |  |         for item in json.loads(upload_queue_json): |  |  |  |         for item in json.loads(upload_queue_json): | 
			
		
	
		
		
			
				
					
					|  |  |  |           upload_queue.put(UploadItem(**item)) |  |  |  |           upload_queue.put(UploadItem.from_dict(item)) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     except Exception: |  |  |  |     except Exception: | 
			
		
	
		
		
			
				
					
					|  |  |  |       cloudlog.exception("athena.UploadQueueCache.initialize.exception") |  |  |  |       cloudlog.exception("athena.UploadQueueCache.initialize.exception") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   @staticmethod |  |  |  |   @staticmethod | 
			
		
	
		
		
			
				
					
					|  |  |  |   def cache(upload_queue): |  |  |  |   def cache(upload_queue: Queue[UploadItem]) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       items = [i._asdict() for i in upload_queue.queue if i.id not in cancelled_uploads] |  |  |  |       queue: List[Optional[UploadItem]] = list(upload_queue.queue) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)] | 
			
		
	
		
		
			
				
					
					|  |  |  |       UploadQueueCache.params.put("AthenadUploadQueue", json.dumps(items)) |  |  |  |       UploadQueueCache.params.put("AthenadUploadQueue", json.dumps(items)) | 
			
		
	
		
		
			
				
					
					|  |  |  |     except Exception: |  |  |  |     except Exception: | 
			
		
	
		
		
			
				
					
					|  |  |  |       cloudlog.exception("athena.UploadQueueCache.cache.exception") |  |  |  |       cloudlog.exception("athena.UploadQueueCache.cache.exception") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def handle_long_poll(ws): |  |  |  | def handle_long_poll(ws: WebSocket) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   end_event = threading.Event() |  |  |  |   end_event = threading.Event() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   threads = [ |  |  |  |   threads = [ | 
			
		
	
	
		
		
			
				
					|  |  | @ -126,7 +165,7 @@ def handle_long_poll(ws): | 
			
		
	
		
		
			
				
					
					|  |  |  |       thread.join() |  |  |  |       thread.join() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def jsonrpc_handler(end_event): |  |  |  | def jsonrpc_handler(end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   dispatcher["startLocalProxy"] = partial(startLocalProxy, end_event) |  |  |  |   dispatcher["startLocalProxy"] = partial(startLocalProxy, end_event) | 
			
		
	
		
		
			
				
					
					|  |  |  |   while not end_event.is_set(): |  |  |  |   while not end_event.is_set(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
	
		
		
			
				
					|  |  | @ -147,11 +186,12 @@ def jsonrpc_handler(end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = True) -> None: |  |  |  | def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = True) -> None: | 
			
		
	
		
		
			
				
					
					|  |  |  |   if cur_upload_items[tid].retry_count < MAX_RETRY_COUNT: |  |  |  |   item = cur_upload_items[tid] | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     item = cur_upload_items[tid] |  |  |  |   if item is not None and item.retry_count < MAX_RETRY_COUNT: | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     new_retry_count = item.retry_count + 1 if increase_count else item.retry_count |  |  |  |     new_retry_count = item.retry_count + 1 if increase_count else item.retry_count | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     item = item._replace( |  |  |  |     item = replace( | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       item, | 
			
		
	
		
		
			
				
					
					|  |  |  |       retry_count=new_retry_count, |  |  |  |       retry_count=new_retry_count, | 
			
		
	
		
		
			
				
					
					|  |  |  |       progress=0, |  |  |  |       progress=0, | 
			
		
	
		
		
			
				
					
					|  |  |  |       current=False |  |  |  |       current=False | 
			
		
	
	
		
		
			
				
					|  |  | @ -175,44 +215,44 @@ def upload_handler(end_event: threading.Event) -> None: | 
			
		
	
		
		
			
				
					
					|  |  |  |     cur_upload_items[tid] = None |  |  |  |     cur_upload_items[tid] = None | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       cur_upload_items[tid] = upload_queue.get(timeout=1)._replace(current=True) |  |  |  |       cur_upload_items[tid] = item = replace(upload_queue.get(timeout=1), current=True) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       if cur_upload_items[tid].id in cancelled_uploads: |  |  |  |       if item.id in cancelled_uploads: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         cancelled_uploads.remove(cur_upload_items[tid].id) |  |  |  |         cancelled_uploads.remove(item.id) | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |         continue |  |  |  |         continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       # Remove item if too old |  |  |  |       # Remove item if too old | 
			
		
	
		
		
			
				
					
					|  |  |  |       age = datetime.now() - datetime.fromtimestamp(cur_upload_items[tid].created_at / 1000) |  |  |  |       age = datetime.now() - datetime.fromtimestamp(item.created_at / 1000) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |       if age.total_seconds() > MAX_AGE: |  |  |  |       if age.total_seconds() > MAX_AGE: | 
			
		
	
		
		
			
				
					
					|  |  |  |         cloudlog.event("athena.upload_handler.expired", item=cur_upload_items[tid], error=True) |  |  |  |         cloudlog.event("athena.upload_handler.expired", item=item, error=True) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |         continue |  |  |  |         continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       # Check if uploading over metered connection is allowed |  |  |  |       # Check if uploading over metered connection is allowed | 
			
		
	
		
		
			
				
					
					|  |  |  |       sm.update(0) |  |  |  |       sm.update(0) | 
			
		
	
		
		
			
				
					
					|  |  |  |       metered = sm['deviceState'].networkMetered |  |  |  |       metered = sm['deviceState'].networkMetered | 
			
		
	
		
		
			
				
					
					|  |  |  |       network_type = sm['deviceState'].networkType.raw |  |  |  |       network_type = sm['deviceState'].networkType.raw | 
			
		
	
		
		
			
				
					
					|  |  |  |       if metered and (not cur_upload_items[tid].allow_cellular): |  |  |  |       if metered and (not item.allow_cellular): | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |         retry_upload(tid, end_event, False) |  |  |  |         retry_upload(tid, end_event, False) | 
			
		
	
		
		
			
				
					
					|  |  |  |         continue |  |  |  |         continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       try: |  |  |  |       try: | 
			
		
	
		
		
			
				
					
					|  |  |  |         def cb(sz, cur): |  |  |  |         def cb(sz: int, cur: int) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |           # Abort transfer if connection changed to metered after starting upload |  |  |  |           # Abort transfer if connection changed to metered after starting upload | 
			
		
	
		
		
			
				
					
					|  |  |  |           sm.update(0) |  |  |  |           sm.update(0) | 
			
		
	
		
		
			
				
					
					|  |  |  |           metered = sm['deviceState'].networkMetered |  |  |  |           metered = sm['deviceState'].networkMetered | 
			
		
	
		
		
			
				
					
					|  |  |  |           if metered and (not cur_upload_items[tid].allow_cellular): |  |  |  |           if metered and (not item.allow_cellular): | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             raise AbortTransferException |  |  |  |             raise AbortTransferException | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |           cur_upload_items[tid] = cur_upload_items[tid]._replace(progress=cur / sz if sz else 1) |  |  |  |           cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         fn = cur_upload_items[tid].path |  |  |  |         fn = item.path | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |         try: |  |  |  |         try: | 
			
		
	
		
		
			
				
					
					|  |  |  |           sz = os.path.getsize(fn) |  |  |  |           sz = os.path.getsize(fn) | 
			
		
	
		
		
			
				
					
					|  |  |  |         except OSError: |  |  |  |         except OSError: | 
			
		
	
		
		
			
				
					
					|  |  |  |           sz = -1 |  |  |  |           sz = -1 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=cur_upload_items[tid].retry_count) |  |  |  |         cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         response = _do_upload(cur_upload_items[tid], cb) |  |  |  |         response = _do_upload(item, cb) | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if response.status_code not in (200, 201, 401, 403, 412): |  |  |  |         if response.status_code not in (200, 201, 401, 403, 412): | 
			
		
	
		
		
			
				
					
					|  |  |  |           cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered) |  |  |  |           cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered) | 
			
		
	
	
		
		
			
				
					|  |  | @ -234,7 +274,7 @@ def upload_handler(end_event: threading.Event) -> None: | 
			
		
	
		
		
			
				
					
					|  |  |  |       cloudlog.exception("athena.upload_handler.exception") |  |  |  |       cloudlog.exception("athena.upload_handler.exception") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def _do_upload(upload_item, callback=None): |  |  |  | def _do_upload(upload_item: UploadItem, callback: Optional[Callable] = None) -> requests.Response: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   path = upload_item.path |  |  |  |   path = upload_item.path | 
			
		
	
		
		
			
				
					
					|  |  |  |   compress = False |  |  |  |   compress = False | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -244,27 +284,25 @@ def _do_upload(upload_item, callback=None): | 
			
		
	
		
		
			
				
					
					|  |  |  |     compress = True |  |  |  |     compress = True | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   with open(path, "rb") as f: |  |  |  |   with open(path, "rb") as f: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     data: BinaryIO | 
			
		
	
		
		
			
				
					
					|  |  |  |     if compress: |  |  |  |     if compress: | 
			
		
	
		
		
			
				
					
					|  |  |  |       cloudlog.event("athena.upload_handler.compress", fn=path, fn_orig=upload_item.path) |  |  |  |       cloudlog.event("athena.upload_handler.compress", fn=path, fn_orig=upload_item.path) | 
			
		
	
		
		
			
				
					
					|  |  |  |       data = bz2.compress(f.read()) |  |  |  |       compressed = bz2.compress(f.read()) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       size = len(data) |  |  |  |       size = len(compressed) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       data = io.BytesIO(data) |  |  |  |       data = io.BytesIO(compressed) | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     else: |  |  |  |     else: | 
			
		
	
		
		
			
				
					
					|  |  |  |       size = os.fstat(f.fileno()).st_size |  |  |  |       size = os.fstat(f.fileno()).st_size | 
			
		
	
		
		
			
				
					
					|  |  |  |       data = f |  |  |  |       data = f | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     if callback: |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       data = CallbackReader(data, callback, size) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     return requests.put(upload_item.url, |  |  |  |     return requests.put(upload_item.url, | 
			
		
	
		
		
			
				
					
					|  |  |  |                         data=data, |  |  |  |                         data=CallbackReader(data, callback, size) if callback else data, | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |                         headers={**upload_item.headers, 'Content-Length': str(size)}, |  |  |  |                         headers={**upload_item.headers, 'Content-Length': str(size)}, | 
			
		
	
		
		
			
				
					
					|  |  |  |                         timeout=30) |  |  |  |                         timeout=30) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | # security: user should be able to request any message from their car |  |  |  | # security: user should be able to request any message from their car | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def getMessage(service=None, timeout=1000): |  |  |  | def getMessage(service: str, timeout: int = 1000) -> Dict: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if service is None or service not in service_list: |  |  |  |   if service is None or service not in service_list: | 
			
		
	
		
		
			
				
					
					|  |  |  |     raise Exception("invalid service") |  |  |  |     raise Exception("invalid service") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -274,7 +312,8 @@ def getMessage(service=None, timeout=1000): | 
			
		
	
		
		
			
				
					
					|  |  |  |   if ret is None: |  |  |  |   if ret is None: | 
			
		
	
		
		
			
				
					
					|  |  |  |     raise TimeoutError |  |  |  |     raise TimeoutError | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   return ret.to_dict() |  |  |  |   # this is because capnp._DynamicStructReader doesn't have typing information | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   return cast(Dict, ret.to_dict()) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
	
		
		
			
				
					|  |  | @ -288,7 +327,7 @@ def getVersion() -> Dict[str, str]: | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def setNavDestination(latitude=0, longitude=0, place_name=None, place_details=None): |  |  |  | def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optional[str] = None, place_details: Optional[str] = None) -> Dict[str, int]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   destination = { |  |  |  |   destination = { | 
			
		
	
		
		
			
				
					
					|  |  |  |     "latitude": latitude, |  |  |  |     "latitude": latitude, | 
			
		
	
		
		
			
				
					
					|  |  |  |     "longitude": longitude, |  |  |  |     "longitude": longitude, | 
			
		
	
	
		
		
			
				
					|  |  | @ -300,8 +339,8 @@ def setNavDestination(latitude=0, longitude=0, place_name=None, place_details=No | 
			
		
	
		
		
			
				
					
					|  |  |  |   return {"success": 1} |  |  |  |   return {"success": 1} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def scan_dir(path, prefix): |  |  |  | def scan_dir(path: str, prefix: str) -> List[str]: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |   files = list() |  |  |  |   files = [] | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   # only walk directories that match the prefix |  |  |  |   # only walk directories that match the prefix | 
			
		
	
		
		
			
				
					
					|  |  |  |   # (glob and friends traverse entire dir tree) |  |  |  |   # (glob and friends traverse entire dir tree) | 
			
		
	
		
		
			
				
					
					|  |  |  |   with os.scandir(path) as i: |  |  |  |   with os.scandir(path) as i: | 
			
		
	
	
		
		
			
				
					|  |  | @ -320,18 +359,18 @@ def scan_dir(path, prefix): | 
			
		
	
		
		
			
				
					
					|  |  |  |   return files |  |  |  |   return files | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def listDataDirectory(prefix=''): |  |  |  | def listDataDirectory(prefix='') -> List[str]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   return scan_dir(ROOT, prefix) |  |  |  |   return scan_dir(ROOT, prefix) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def reboot(): |  |  |  | def reboot() -> Dict[str, int]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   sock = messaging.sub_sock("deviceState", timeout=1000) |  |  |  |   sock = messaging.sub_sock("deviceState", timeout=1000) | 
			
		
	
		
		
			
				
					
					|  |  |  |   ret = messaging.recv_one(sock) |  |  |  |   ret = messaging.recv_one(sock) | 
			
		
	
		
		
			
				
					
					|  |  |  |   if ret is None or ret.deviceState.started: |  |  |  |   if ret is None or ret.deviceState.started: | 
			
		
	
		
		
			
				
					
					|  |  |  |     raise Exception("Reboot unavailable") |  |  |  |     raise Exception("Reboot unavailable") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   def do_reboot(): |  |  |  |   def do_reboot() -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     time.sleep(2) |  |  |  |     time.sleep(2) | 
			
		
	
		
		
			
				
					
					|  |  |  |     HARDWARE.reboot() |  |  |  |     HARDWARE.reboot() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -341,50 +380,53 @@ def reboot(): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def uploadFileToUrl(fn, url, headers): |  |  |  | def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesToUrlResponse: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |   return uploadFilesToUrls([{ |  |  |  |   # this is because mypy doesn't understand that the decorator doesn't change the return type | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   response: UploadFilesToUrlResponse = uploadFilesToUrls([{ | 
			
		
	
		
		
			
				
					
					|  |  |  |     "fn": fn, |  |  |  |     "fn": fn, | 
			
		
	
		
		
			
				
					
					|  |  |  |     "url": url, |  |  |  |     "url": url, | 
			
		
	
		
		
			
				
					
					|  |  |  |     "headers": headers, |  |  |  |     "headers": headers, | 
			
		
	
		
		
			
				
					
					|  |  |  |   }]) |  |  |  |   }]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   return response | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def uploadFilesToUrls(files_data): |  |  |  | def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlResponse: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |   items = [] |  |  |  |   files = map(UploadFile.from_dict, files_data) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |   failed = [] |  |  |  | 
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |   for file in files_data: |  |  |  |   items: List[UploadItemDict] = [] | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     fn = file.get('fn', '') |  |  |  |   failed: List[str] = [] | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     if len(fn) == 0 or fn[0] == '/' or '..' in fn or 'url' not in file: |  |  |  |   for file in files: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       failed.append(fn) |  |  |  |     if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0: | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       failed.append(file.fn) | 
			
		
	
		
		
			
				
					
					|  |  |  |       continue |  |  |  |       continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     path = os.path.join(ROOT, fn) |  |  |  |     path = os.path.join(ROOT, file.fn) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     if not os.path.exists(path) and not os.path.exists(strip_bz2_extension(path)): |  |  |  |     if not os.path.exists(path) and not os.path.exists(strip_bz2_extension(path)): | 
			
		
	
		
		
			
				
					
					|  |  |  |       failed.append(fn) |  |  |  |       failed.append(file.fn) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |       continue |  |  |  |       continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     # Skip item if already in queue |  |  |  |     # Skip item if already in queue | 
			
		
	
		
		
			
				
					
					|  |  |  |     url = file['url'].split('?')[0] |  |  |  |     url = file.url.split('?')[0] | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     if any(url == item['url'].split('?')[0] for item in listUploadQueue()): |  |  |  |     if any(url == item['url'].split('?')[0] for item in listUploadQueue()): | 
			
		
	
		
		
			
				
					
					|  |  |  |       continue |  |  |  |       continue | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     item = UploadItem( |  |  |  |     item = UploadItem( | 
			
		
	
		
		
			
				
					
					|  |  |  |       path=path, |  |  |  |       path=path, | 
			
		
	
		
		
			
				
					
					|  |  |  |       url=file['url'], |  |  |  |       url=file.url, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       headers=file.get('headers', {}), |  |  |  |       headers=file.headers, | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |       created_at=int(time.time() * 1000), |  |  |  |       created_at=int(time.time() * 1000), | 
			
		
	
		
		
			
				
					
					|  |  |  |       id=None, |  |  |  |       id=None, | 
			
		
	
		
		
			
				
					
					|  |  |  |       allow_cellular=file.get('allow_cellular', False), |  |  |  |       allow_cellular=file.allow_cellular, | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     ) |  |  |  |     ) | 
			
		
	
		
		
			
				
					
					|  |  |  |     upload_id = hashlib.sha1(str(item).encode()).hexdigest() |  |  |  |     upload_id = hashlib.sha1(str(item).encode()).hexdigest() | 
			
		
	
		
		
			
				
					
					|  |  |  |     item = item._replace(id=upload_id) |  |  |  |     item = replace(item, id=upload_id) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     upload_queue.put_nowait(item) |  |  |  |     upload_queue.put_nowait(item) | 
			
		
	
		
		
			
				
					
					|  |  |  |     items.append(item._asdict()) |  |  |  |     items.append(asdict(item)) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   UploadQueueCache.cache(upload_queue) |  |  |  |   UploadQueueCache.cache(upload_queue) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   resp = {"enqueued": len(items), "items": items} |  |  |  |   resp: UploadFilesToUrlResponse = {"enqueued": len(items), "items": items} | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if failed: |  |  |  |   if failed: | 
			
		
	
		
		
			
				
					
					|  |  |  |     resp["failed"] = failed |  |  |  |     resp["failed"] = failed | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -392,32 +434,32 @@ def uploadFilesToUrls(files_data): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def listUploadQueue(): |  |  |  | def listUploadQueue() -> List[UploadItemDict]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   items = list(upload_queue.queue) + list(cur_upload_items.values()) |  |  |  |   items = list(upload_queue.queue) + list(cur_upload_items.values()) | 
			
		
	
		
		
			
				
					
					|  |  |  |   return [i._asdict() for i in items if (i is not None) and (i.id not in cancelled_uploads)] |  |  |  |   return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)] | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def cancelUpload(upload_id): |  |  |  | def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if not isinstance(upload_id, list): |  |  |  |   if not isinstance(upload_id, list): | 
			
		
	
		
		
			
				
					
					|  |  |  |     upload_id = [upload_id] |  |  |  |     upload_id = [upload_id] | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   uploading_ids = {item.id for item in list(upload_queue.queue)} |  |  |  |   uploading_ids = {item.id for item in list(upload_queue.queue)} | 
			
		
	
		
		
			
				
					
					|  |  |  |   cancelled_ids = uploading_ids.intersection(upload_id) |  |  |  |   cancelled_ids = uploading_ids.intersection(upload_id) | 
			
		
	
		
		
			
				
					
					|  |  |  |   if len(cancelled_ids) == 0: |  |  |  |   if len(cancelled_ids) == 0: | 
			
		
	
		
		
			
				
					
					|  |  |  |     return 404 |  |  |  |     return {"success": 0, "error": "not found"} | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   cancelled_uploads.update(cancelled_ids) |  |  |  |   cancelled_uploads.update(cancelled_ids) | 
			
		
	
		
		
			
				
					
					|  |  |  |   return {"success": 1} |  |  |  |   return {"success": 1} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def primeActivated(activated): |  |  |  | def primeActivated(activated: bool) -> Dict[str, int]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   return {"success": 1} |  |  |  |   return {"success": 1} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def setBandwithLimit(upload_speed_kbps, download_speed_kbps): |  |  |  | def setBandwithLimit(upload_speed_kbps: int, download_speed_kbps: int) -> Dict[str, Union[int, str]]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if not AGNOS: |  |  |  |   if not AGNOS: | 
			
		
	
		
		
			
				
					
					|  |  |  |     return {"success": 0, "error": "only supported on AGNOS"} |  |  |  |     return {"success": 0, "error": "only supported on AGNOS"} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -428,7 +470,7 @@ def setBandwithLimit(upload_speed_kbps, download_speed_kbps): | 
			
		
	
		
		
			
				
					
					|  |  |  |     return {"success": 0, "error": "failed to set limit", "stdout": e.stdout, "stderr": e.stderr} |  |  |  |     return {"success": 0, "error": "failed to set limit", "stdout": e.stdout, "stderr": e.stderr} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def startLocalProxy(global_end_event, remote_ws_uri, local_port): |  |  |  | def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> Dict[str, int]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   try: |  |  |  |   try: | 
			
		
	
		
		
			
				
					
					|  |  |  |     if local_port not in LOCAL_PORT_WHITELIST: |  |  |  |     if local_port not in LOCAL_PORT_WHITELIST: | 
			
		
	
		
		
			
				
					
					|  |  |  |       raise Exception("Requested local port not whitelisted") |  |  |  |       raise Exception("Requested local port not whitelisted") | 
			
		
	
	
		
		
			
				
					|  |  | @ -462,7 +504,7 @@ def startLocalProxy(global_end_event, remote_ws_uri, local_port): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def getPublicKey(): |  |  |  | def getPublicKey() -> Optional[str]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if not os.path.isfile(PERSIST + '/comma/id_rsa.pub'): |  |  |  |   if not os.path.isfile(PERSIST + '/comma/id_rsa.pub'): | 
			
		
	
		
		
			
				
					
					|  |  |  |     return None |  |  |  |     return None | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -471,7 +513,7 @@ def getPublicKey(): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def getSshAuthorizedKeys(): |  |  |  | def getSshAuthorizedKeys() -> str: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   return Params().get("GithubSshKeys", encoding='utf8') or '' |  |  |  |   return Params().get("GithubSshKeys", encoding='utf8') or '' | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -486,7 +528,7 @@ def getNetworkType(): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def getNetworkMetered(): |  |  |  | def getNetworkMetered() -> bool: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   network_type = HARDWARE.get_network_type() |  |  |  |   network_type = HARDWARE.get_network_type() | 
			
		
	
		
		
			
				
					
					|  |  |  |   return HARDWARE.get_network_metered(network_type) |  |  |  |   return HARDWARE.get_network_metered(network_type) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -497,7 +539,7 @@ def getNetworks(): | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | @dispatcher.add_method |  |  |  | @dispatcher.add_method | 
			
		
	
		
		
			
				
					
					|  |  |  | def takeSnapshot(): |  |  |  | def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   from system.camerad.snapshot.snapshot import jpeg_write, snapshot |  |  |  |   from system.camerad.snapshot.snapshot import jpeg_write, snapshot | 
			
		
	
		
		
			
				
					
					|  |  |  |   ret = snapshot() |  |  |  |   ret = snapshot() | 
			
		
	
		
		
			
				
					
					|  |  |  |   if ret is not None: |  |  |  |   if ret is not None: | 
			
		
	
	
		
		
			
				
					|  |  | @ -514,16 +556,19 @@ def takeSnapshot(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     raise Exception("not available while camerad is started") |  |  |  |     raise Exception("not available while camerad is started") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def get_logs_to_send_sorted(): |  |  |  | def get_logs_to_send_sorted() -> List[str]: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   # TODO: scan once then use inotify to detect file creation/deletion |  |  |  |   # TODO: scan once then use inotify to detect file creation/deletion | 
			
		
	
		
		
			
				
					
					|  |  |  |   curr_time = int(time.time()) |  |  |  |   curr_time = int(time.time()) | 
			
		
	
		
		
			
				
					
					|  |  |  |   logs = [] |  |  |  |   logs = [] | 
			
		
	
		
		
			
				
					
					|  |  |  |   for log_entry in os.listdir(SWAGLOG_DIR): |  |  |  |   for log_entry in os.listdir(SWAGLOG_DIR): | 
			
		
	
		
		
			
				
					
					|  |  |  |     log_path = os.path.join(SWAGLOG_DIR, log_entry) |  |  |  |     log_path = os.path.join(SWAGLOG_DIR, log_entry) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     time_sent = 0 | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       time_sent = int.from_bytes(getxattr(log_path, LOG_ATTR_NAME), sys.byteorder) |  |  |  |       value = getxattr(log_path, LOG_ATTR_NAME) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       if value is not None: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         time_sent = int.from_bytes(value, sys.byteorder) | 
			
		
	
		
		
			
				
					
					|  |  |  |     except (ValueError, TypeError): |  |  |  |     except (ValueError, TypeError): | 
			
		
	
		
		
			
				
					
					|  |  |  |       time_sent = 0 |  |  |  |       pass | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     # assume send failed and we lost the response if sent more than one hour ago |  |  |  |     # assume send failed and we lost the response if sent more than one hour ago | 
			
		
	
		
		
			
				
					
					|  |  |  |     if not time_sent or curr_time - time_sent > 3600: |  |  |  |     if not time_sent or curr_time - time_sent > 3600: | 
			
		
	
		
		
			
				
					
					|  |  |  |       logs.append(log_entry) |  |  |  |       logs.append(log_entry) | 
			
		
	
	
		
		
			
				
					|  |  | @ -531,7 +576,7 @@ def get_logs_to_send_sorted(): | 
			
		
	
		
		
			
				
					
					|  |  |  |   return sorted(logs)[:-1] |  |  |  |   return sorted(logs)[:-1] | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def log_handler(end_event): |  |  |  | def log_handler(end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   if PC: |  |  |  |   if PC: | 
			
		
	
		
		
			
				
					
					|  |  |  |     return |  |  |  |     return | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -593,7 +638,7 @@ def log_handler(end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |       cloudlog.exception("athena.log_handler.exception") |  |  |  |       cloudlog.exception("athena.log_handler.exception") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def stat_handler(end_event): |  |  |  | def stat_handler(end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   while not end_event.is_set(): |  |  |  |   while not end_event.is_set(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     last_scan = 0 |  |  |  |     last_scan = 0 | 
			
		
	
		
		
			
				
					
					|  |  |  |     curr_scan = sec_since_boot() |  |  |  |     curr_scan = sec_since_boot() | 
			
		
	
	
		
		
			
				
					|  |  | @ -619,7 +664,7 @@ def stat_handler(end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |     time.sleep(0.1) |  |  |  |     time.sleep(0.1) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def ws_proxy_recv(ws, local_sock, ssock, end_event, global_end_event): |  |  |  | def ws_proxy_recv(ws: WebSocket, local_sock: socket.socket, ssock: socket.socket, end_event: threading.Event, global_end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   while not (end_event.is_set() or global_end_event.is_set()): |  |  |  |   while not (end_event.is_set() or global_end_event.is_set()): | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       data = ws.recv() |  |  |  |       data = ws.recv() | 
			
		
	
	
		
		
			
				
					|  |  | @ -638,7 +683,7 @@ def ws_proxy_recv(ws, local_sock, ssock, end_event, global_end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |   end_event.set() |  |  |  |   end_event.set() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def ws_proxy_send(ws, local_sock, signal_sock, end_event): |  |  |  | def ws_proxy_send(ws: WebSocket, local_sock: socket.socket, signal_sock: socket.socket, end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   while not end_event.is_set(): |  |  |  |   while not end_event.is_set(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       r, _, _ = select.select((local_sock, signal_sock), (), ()) |  |  |  |       r, _, _ = select.select((local_sock, signal_sock), (), ()) | 
			
		
	
	
		
		
			
				
					|  |  | @ -663,7 +708,7 @@ def ws_proxy_send(ws, local_sock, signal_sock, end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |   cloudlog.debug("athena.ws_proxy_send done closing sockets") |  |  |  |   cloudlog.debug("athena.ws_proxy_send done closing sockets") | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def ws_recv(ws, end_event): |  |  |  | def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   last_ping = int(sec_since_boot() * 1e9) |  |  |  |   last_ping = int(sec_since_boot() * 1e9) | 
			
		
	
		
		
			
				
					
					|  |  |  |   while not end_event.is_set(): |  |  |  |   while not end_event.is_set(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
	
		
		
			
				
					|  |  | @ -685,7 +730,7 @@ def ws_recv(ws, end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |       end_event.set() |  |  |  |       end_event.set() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def ws_send(ws, end_event): |  |  |  | def ws_send(ws: WebSocket, end_event: threading.Event) -> None: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   while not end_event.is_set(): |  |  |  |   while not end_event.is_set(): | 
			
		
	
		
		
			
				
					
					|  |  |  |     try: |  |  |  |     try: | 
			
		
	
		
		
			
				
					
					|  |  |  |       try: |  |  |  |       try: | 
			
		
	
	
		
		
			
				
					|  |  | @ -704,7 +749,7 @@ def ws_send(ws, end_event): | 
			
		
	
		
		
			
				
					
					|  |  |  |       end_event.set() |  |  |  |       end_event.set() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | def backoff(retries): |  |  |  | def backoff(retries: int) -> int: | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   return random.randrange(0, min(128, int(2 ** retries))) |  |  |  |   return random.randrange(0, min(128, int(2 ** retries))) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | 
 |