#!/usr/bin/env python3 # Copyright(c) 2022 Intel Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import os import sys import struct import logging import time import subprocess import argparse import socketserver import threading import hashlib import queue from urllib.parse import urlparse # Global variable use to sync between log and request services. runner = None # pylint: disable=duplicate-code # INADDR_ANY as default HOST = '' PORT_LOG = 9999 PORT_REQ = PORT_LOG + 1 BUF_SIZE = 4096 # Define the command and the max size CMD_LOG_START = "start_log" CMD_DOWNLOAD = "download" MAX_CMD_SZ = 16 # Define the return value in handle function ERR_FAIL = 1 # Define the header format and size for # transmiting the firmware PACKET_HEADER_FORMAT_FW = 'I 42s 32s' HEADER_SZ = 78 logging.basicConfig(level=logging.INFO) log = logging.getLogger("remote-fw") class adsp_request_handler(socketserver.BaseRequestHandler): """ The request handler class for control the actions of server. """ def receive_fw(self): log.info("Receiving...") # Receive the header first d = self.request.recv(HEADER_SZ) # Unpacked the header data # Include size(4), filename(42) and MD5(32) header = d[:HEADER_SZ] total = d[HEADER_SZ:] s = struct.Struct(PACKET_HEADER_FORMAT_FW) fsize, fname, md5_tx_b = s.unpack(header) log.info(f'size:{fsize}, filename:{fname}, MD5:{md5_tx_b}') # Receive the firmware. We only receive the specified amount of bytes. while len(total) < fsize: data = self.request.recv(min(BUF_SIZE, fsize - len(total))) if not data: raise EOFError("truncated firmware file") total += data log.info(f"Done Receiving {len(total)}.") try: with open(fname,'wb') as f: f.write(total) except Exception as e: log.error(f"Get exception {e} during FW transfer.") return None # Check the MD5 of the firmware md5_rx = hashlib.md5(total).hexdigest() md5_tx = md5_tx_b.decode('utf-8') if md5_tx != md5_rx: log.error(f'MD5 mismatch: {md5_tx} vs. {md5_rx}') return None return fname def do_download(self): recv_file = self.receive_fw() if recv_file: recv_file = recv_file.decode('utf-8') if os.path.exists(recv_file): runner.set_fw_ready(recv_file) return 0 log.error("Cannot find the FW file.") return ERR_FAIL def handle(self): cmd = self.request.recv(MAX_CMD_SZ) log.info(f"{self.client_address[0]} wrote: {cmd}") action = cmd.decode("utf-8") log.debug(f'load {action}') ret = ERR_FAIL if action == CMD_DOWNLOAD: self.request.sendall(cmd) ret = self.do_download() else: log.error("incorrect load communitcation!") return if not ret: self.request.sendall("success".encode('utf-8')) log.info("Firmware well received. Ready to download.") else: self.request.sendall("failed".encode('utf-8')) log.error("Receive firmware failed.") class adsp_log_handler(socketserver.BaseRequestHandler): """ The log handler class for grabbing output messages of server. """ def handle(self): cmd = self.request.recv(MAX_CMD_SZ) log.info(f"{self.client_address[0]} wrote: {cmd}") action = cmd.decode("utf-8") log.debug(f'monitor {action}') if action == CMD_LOG_START: self.request.sendall(cmd) else: log.error("incorrect monitor communitcation!") log.info("wait for FW ready...") while not runner.is_fw_ready(): if not self.is_connection_alive(): return time.sleep(1) log.info("FW is ready...") # start_new_session=True in order to get a different Process Group # ID. When the PGID is the same, sudo does NOT propagate signals out of # fear of "accidentally killing itself" (man sudo). # Compare: # # - Different PGID: signal is propagated and sleep is terminated # # sudo sleep 15 & kill $! # # - Same PGID, sleep is NOT terminated # # sudo bash -c 'sleep 15 & killall sudo' # # ps xfao pid,ppid,pgid,sid,comm | grep -C 5 -e PID -e sleep -e sudo with subprocess.Popen(runner.get_script(), stdout=subprocess.PIPE, start_new_session=True) as proc: # Thread for monitoring the conntection t = threading.Thread(target=self.check_connection, args=(proc,)) t.start() while True: try: out = proc.stdout.readline() self.request.sendall(out) ret = proc.poll() if ret: log.info(f"retrun code: {ret}") break except (BrokenPipeError, ConnectionResetError): log.info("Client is disconnect.") break t.join() log.info("service complete.") def finish(self): runner.cleanup() log.info("Wait for next service...") def is_connection_alive(self): try: self.request.sendall(b'\x00') except (BrokenPipeError, ConnectionResetError): log.info("Client is disconnect.") return False return True def check_connection(self, proc): # Not to check connection alive for # the first 10 secs. time.sleep(10) poll_interval = 1 log.info("Now checking client connection every %ds", poll_interval) while True: if not self.is_connection_alive(): # cavstool child_desc = " ".join(runner.script) + f", PID={proc.pid}" log.info("Terminating %s", child_desc) try: # sudo does _not_ propagate SIGKILL (man sudo) proc.terminate() try: proc.wait(timeout=0.5) except subprocess.TimeoutExpired: log.error("SIGTERM failed on child %s", child_desc) if os.geteuid() == 0: # sudo not needed and not used log.error("Sending %d SIGKILL", proc.pid) proc.kill() else: log.error("Try: sudo pkill -9 -f %s", runner.load_cmd) except PermissionError: log.info("cannot kill proc due to it start with sudo...") os.system(f"sudo kill -9 {proc.pid} ") return time.sleep(poll_interval) class device_runner(): def __init__(self, args): self.fw_file = None self.lock = threading.Lock() self.fw_queue = queue.Queue() # Board specific config self.board = board_config(args) self.load_cmd = self.board.get_cmd() def set_fw_ready(self, fw_recv): if fw_recv: self.fw_queue.put(fw_recv) def is_fw_ready(self): self.fw_file = self.fw_queue.get() log.info(f"Current FW is {self.fw_file}") return bool(self.fw_file) def cleanup(self): self.lock.acquire() self.script = None if self.fw_file: os.remove(self.fw_file) self.fw_file = None self.lock.release() def get_script(self): if os.geteuid() != 0: self.script = [f'sudo', f'{self.load_cmd}'] else: self.script = [f'{self.load_cmd}'] self.script.append(f'{self.fw_file}') if self.board.params: for param in self.board.params: self.script.append(param) log.info(f'run script: {self.script}') return self.script class board_config(): def __init__(self, args): self.load_cmd = args.load_cmd # cmd for loading self.params = [] # params of loading cmd if not self.load_cmd: self.load_cmd = "./cavstool.py" if not self.load_cmd or not os.path.exists(self.load_cmd): log.error(f'Cannot find load cmd {self.load_cmd}.') sys.exit(1) def get_cmd(self): return self.load_cmd def get_params(self): return self.params ap = argparse.ArgumentParser(description="RemoteHW service tool", allow_abbrev=False) ap.add_argument("-q", "--quiet", action="store_true", help="No loader output, just DSP logging") ap.add_argument("-v", "--verbose", action="store_true", help="More loader output, DEBUG logging level") ap.add_argument("-s", "--server-addr", help="Specify the only IP address the log server will LISTEN on") ap.add_argument("-p", "--log-port", help="Specify the PORT that the log server to active") ap.add_argument("-r", "--req-port", help="Specify the PORT that the request server to active") ap.add_argument("-c", "--load-cmd", help="Specify loading command of the board") args = ap.parse_args() if args.quiet: log.setLevel(logging.WARN) elif args.verbose: log.setLevel(logging.DEBUG) if args.server_addr: url = urlparse("//" + args.server_addr) if url.hostname: HOST = url.hostname if url.port: PORT_LOG = int(url.port) if args.log_port: PORT_LOG = int(args.log_port) if args.req_port: PORT_REQ = int(args.req_port) log.info(f"Serve on LOG PORT: {PORT_LOG} REQ PORT: {PORT_REQ}") if __name__ == "__main__": # Do board configuration setup runner = device_runner(args) # Launch the command request service socketserver.TCPServer.allow_reuse_address = True req_server = socketserver.TCPServer((HOST, PORT_REQ), adsp_request_handler) req_t = threading.Thread(target=req_server.serve_forever, daemon=True) # Activate the log service which output board's execution result log_server = socketserver.TCPServer((HOST, PORT_LOG), adsp_log_handler) log_t = threading.Thread(target=log_server.serve_forever, daemon=True) try: log.info("Req server start...") req_t.start() log.info("Log server start...") log_t.start() req_t.join() log_t.join() except KeyboardInterrupt: log_server.shutdown() req_server.shutdown()