@ -4,6 +4,7 @@ import hashlib
import io
import json
import os
import sys
import queue
import random
import select
@ -24,18 +25,24 @@ from common.api import Api
from common . basedir import PERSIST
from common . params import Params
from common . realtime import sec_since_boot
from selfdrive . hardware import HARDWARE
from selfdrive . hardware import HARDWARE , PC
from selfdrive . loggerd . config import ROOT
from selfdrive . swaglog import cloudlog
from selfdrive . loggerd . xattr_cache import getxattr , setxattr
from selfdrive . swaglog import cloudlog , SWAGLOG_DIR
ATHENA_HOST = os . getenv ( ' ATHENA_HOST ' , ' wss://athena.comma.ai ' )
HANDLER_THREADS = int ( os . getenv ( ' HANDLER_THREADS ' , " 4 " ) )
LOCAL_PORT_WHITELIST = set ( [ 8022 ] )
LOG_ATTR_NAME = ' user.upload '
LOG_ATTR_VALUE_MAX_UNIX_TIME = int . to_bytes ( 2147483647 , 4 , sys . byteorder )
dispatcher [ " echo " ] = lambda s : s
payload_queue : Any = queue . Queue ( )
response_queue : Any = queue . Queue ( )
recv _queue: Any = queue . Queue ( )
send _queue : Any = queue . Queue ( )
upload_queue : Any = queue . Queue ( )
log_send_queue : Any = queue . Queue ( )
log_recv_queue : Any = queue . Queue ( )
cancelled_uploads : Any = set ( )
UploadItem = namedtuple ( ' UploadItem ' , [ ' path ' , ' url ' , ' headers ' , ' created_at ' , ' id ' ] )
@ -46,7 +53,8 @@ def handle_long_poll(ws):
threads = [
threading . Thread ( target = ws_recv , args = ( ws , end_event ) ) ,
threading . Thread ( target = ws_send , args = ( ws , end_event ) ) ,
threading . Thread ( target = upload_handler , args = ( end_event , ) )
threading . Thread ( target = upload_handler , args = ( end_event , ) ) ,
threading . Thread ( target = log_handler , args = ( end_event , ) ) ,
] + [
threading . Thread ( target = jsonrpc_handler , args = ( end_event , ) )
for x in range ( HANDLER_THREADS )
@ -64,19 +72,21 @@ def handle_long_poll(ws):
for thread in threads :
thread . join ( )
def jsonrpc_handler ( end_event ) :
dispatcher [ " startLocalProxy " ] = partial ( startLocalProxy , end_event )
while not end_event . is_set ( ) :
try :
data = payload_queue . get ( timeout = 1 )
response = JSONRPCResponseManager . handle ( data , dispatcher )
response_queue . put_nowait ( response )
data = recv_queue . get ( timeout = 1 )
if " method " in data and " params " in data :
response = JSONRPCResponseManager . handle ( data , dispatcher )
send_queue . put_nowait ( response . json )
elif " result " in data and " id " in data :
log_recv_queue . put_nowait ( data )
except queue . Empty :
pass
except Exception as e :
cloudlog . exception ( " athena jsonrpc handler failed " )
respon se_queue. put_nowait ( json . dumps ( { " error " : str ( e ) } ) )
send _queue . put_nowait ( json . dumps ( { " error " : str ( e ) } ) )
def upload_handler ( end_event ) :
@ -244,6 +254,82 @@ def takeSnapshot():
raise Exception ( " not available while camerad is started " )
def get_logs_to_send_sorted ( ) :
# TODO: scan once then use inotify to detect file creation/deletion
curr_time = int ( time . time ( ) )
logs = [ ]
for log_entry in os . listdir ( SWAGLOG_DIR ) :
log_path = os . path . join ( SWAGLOG_DIR , log_entry )
try :
time_sent = int . from_bytes ( getxattr ( log_path , LOG_ATTR_NAME ) , sys . byteorder )
except ( ValueError , TypeError ) :
time_sent = 0
# 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 :
logs . append ( log_entry )
# return logs in order they should be sent
# excluding most recent (active) log file
return sorted ( logs [ : - 1 ] )
def log_handler ( end_event ) :
if PC :
return
log_files = [ ]
last_scan = 0
log_retries = 0
while not end_event . is_set ( ) :
try :
try :
result = json . loads ( log_recv_queue . get ( timeout = 1 ) )
log_success = result . get ( " success " )
log_entry = result . get ( " id " )
log_path = os . path . join ( SWAGLOG_DIR , log_entry )
if log_entry and log_success :
try :
setxattr ( log_path , LOG_ATTR_NAME , LOG_ATTR_VALUE_MAX_UNIX_TIME )
except OSError :
pass # file could be deleted by log rotation
except queue . Empty :
pass
curr_scan = sec_since_boot ( )
if curr_scan - last_scan > 10 :
log_files = get_logs_to_send_sorted ( )
last_scan = curr_scan
# never send last log file because it is the active log
# and only send one log file at a time (most recent first)
if not len ( log_files ) or not log_send_queue . empty ( ) :
continue
log_entry = log_files . pop ( )
try :
curr_time = int ( time . time ( ) )
log_path = os . path . join ( SWAGLOG_DIR , log_entry )
setxattr ( log_path , LOG_ATTR_NAME , int . to_bytes ( curr_time , 4 , sys . byteorder ) )
with open ( log_path , " r " ) as f :
jsonrpc = {
" method " : " forwardLogs " ,
" params " : {
" logs " : f . read ( )
} ,
" jsonrpc " : " 2.0 " ,
" id " : log_entry
}
log_send_queue . put_nowait ( json . dumps ( jsonrpc ) )
except OSError :
pass # file could be deleted by log rotation
log_retries = 0
except Exception :
cloudlog . exception ( " athena.log_handler.exception " )
log_retries + = 1
if log_retries != 0 :
time . sleep ( backoff ( log_retries ) )
def ws_proxy_recv ( ws , local_sock , ssock , end_event , global_end_event ) :
while not ( end_event . is_set ( ) or global_end_event . is_set ( ) ) :
try :
@ -290,7 +376,7 @@ def ws_recv(ws, end_event):
if opcode in ( ABNF . OPCODE_TEXT , ABNF . OPCODE_BINARY ) :
if opcode == ABNF . OPCODE_TEXT :
data = data . decode ( " utf-8 " )
payload _queue. put_nowait ( data )
recv _queue. put_nowait ( data )
elif opcode == ABNF . OPCODE_PING :
Params ( ) . put ( " LastAthenaPingTime " , str ( int ( sec_since_boot ( ) * 1e9 ) ) )
except WebSocketTimeoutException :
@ -303,8 +389,11 @@ def ws_recv(ws, end_event):
def ws_send ( ws , end_event ) :
while not end_event . is_set ( ) :
try :
response = response_queue . get ( timeout = 1 )
ws . send ( response . json )
try :
data = send_queue . get_nowait ( )
except queue . Empty :
data = log_send_queue . get ( timeout = 1 )
ws . send ( data )
except queue . Empty :
pass
except Exception :