| #!/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 |
| |
| # 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() |