diff --git a/tools/otagui/ota_interface.py b/tools/otagui/ota_interface.py index f5b5fb036..ec0490629 100644 --- a/tools/otagui/ota_interface.py +++ b/tools/otagui/ota_interface.py @@ -2,52 +2,170 @@ import subprocess import os import json import pipes -from threading import Lock +import threading +from dataclasses import dataclass, asdict, field import logging +import sqlite3 +import time + + +@dataclass +class JobInfo: + """ + A class for ota job information + """ + id: str + target: str + incremental: str = '' + verbose: bool = False + partial: list[str] = field(default_factory=list) + output: str = '' + status: str = 'Running' + downgrade: bool = False + extra: str = '' + stdout: str = '' + stderr: str = '' + start_time: int = 0 + finish_time: int = 0 + isPartial: bool = False + isIncremental: bool = False + + def __post_init__(self): + if not self.output: + self.output = os.path.join('output', self.id, '.zip') + if not self.stdout: + self.stdout = os.path.join('output/stdout.'+self.id) + if not self.stderr: + self.stderr = os.path.join('output/stderr.'+self.id) + + def enforce_bool(t): return t if isinstance(t, bool) else bool(t) + self.verbose, self.downgrade = map( + enforce_bool, + [self.verbose, self.downgrade]) + if self.incremental: + self.isIncremental = True + if self.partial: + self.isPartial = True + + def to_sql_form_dict(self): + sql_form_dict = asdict(self) + sql_form_dict['partial'] = ','.join(sql_form_dict['partial']) + def bool_to_int(t): return 1 if t else 0 + sql_form_dict['verbose'], sql_form_dict['downgrade'] = map( + bool_to_int, + [sql_form_dict['verbose'], sql_form_dict['downgrade']]) + return sql_form_dict + + def to_dict_basic(self): + basic_info = asdict(self) + basic_info['target_name'] = self.target.split('/')[-1] + if self.isIncremental: + basic_info['incremental_name'] = self.incremental.split('/')[-1] + return basic_info + + def to_dict_detail(self, target_lib, offset=0): + detail_info = asdict(self) + try: + with open(self.stdout, 'r') as fout: + detail_info['stdout'] = fout.read() + with open(self.stderr, 'r') as ferr: + detail_info['stderr'] = ferr.read() + except FileNotFoundError: + detail_info['stdout'] = 'NO STD OUTPUT IS FOUND' + detail_info['stderr'] = 'NO STD ERROR IS FOUND' + target_info = target_lib.get_build_by_path(self.target) + detail_info['target_name'] = target_info.file_name + detail_info['target_build_version'] = target_info.build_version + if self.incremental: + incremental_info = target_lib.get_build_by_path( + self.incremental) + detail_info['incremental_name'] = incremental_info.file_name + detail_info['incremental_build_version'] = incremental_info.build_version + return detail_info + class ProcessesManagement: - def __init__(self): - self.__container = {} - self.__lock = Lock() + """ + A class manage the ota generate process + """ - def set(self, name, value): - with self.__lock: - self.__container[name] = value + def __init__(self, path='ota_database.db'): + """ + create a table if not exist + """ + self.path = path + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + CREATE TABLE if not exists Jobs ( + ID TEXT, + TargetPath TEXT, + IncrementalPath TEXT, + Verbose INTEGER, + Partial TEXT, + OutputPath TEXT, + Status TEXT, + Downgrade INTEGER, + OtherFlags TEXT, + STDOUT TEXT, + STDERR TEXT, + StartTime INTEGER, + FinishTime INTEGER + ) + """) - def get(self, name): - with self.__lock: - return self.__container[name] - - def get_keys(self): - with self.__lock: - return self.__container.keys() - - def get_status_by_ID(self, id=0, details=False): - status = {} - if not id in self.get_keys(): - return '{}' - else: - status['id'] = id - if self.get(id).poll() == None: - status['status'] = 'Running' - elif self.get(id).poll() == 0: - status['status'] = 'Finished' - status['path'] = os.path.join('output', str(id) + '.zip') - else: - status['status'] = 'Error' - try: - if details: - with open(os.path.join('output', 'stdout.' + str(id)), 'r') as fout: - status['stdout'] = fout.read() - with open(os.path.join('output', 'stderr.' + str(id)), 'r') as ferr: - status['stderr'] = ferr.read() - except FileNotFoundError: - status['stdout'] = 'NO STD OUTPUT IS FOUND' - status['stderr'] = 'NO STD OUTPUT IS FOUND' - return status + def get_status_by_ID(self, id): + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + logging.info(id) + cursor.execute(""" + SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime + FROM Jobs WHERE ID=(?) + """, (id,)) + row = cursor.fetchone() + status = JobInfo(*row) + return status def get_status(self): - return [self.get_status_by_ID(id=id) for id in self.get_keys()] + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime + FROM Jobs + """) + rows = cursor.fetchall() + statuses = [JobInfo(*row) for row in rows] + return statuses + + def update_status(self, id, status, finish_time): + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + UPDATE Jobs SET Status=(?), FinishTime=(?) + WHERE ID=(?) + """, + (status, finish_time, id)) + + def ota_run(self, command, id): + # Start a subprocess and collect the output + stderr_pipes = pipes.Template() + stdout_pipes = pipes.Template() + ferr = stderr_pipes.open(os.path.join( + 'output', 'stderr.'+str(id)), 'w') + fout = stdout_pipes.open(os.path.join( + 'output', 'stdout.'+str(id)), 'w') + try: + proc = subprocess.Popen( + command, stderr=ferr, stdout=fout) + except FileNotFoundError: + logging.error('ota_from_target_files is not set properly') + self.update_status(id, 'Error', int(time.time())) + return + exit_code = proc.wait() + if exit_code == 0: + self.update_status(id, 'Finished', int(time.time())) + else: + self.update_status(id, 'Error', int(time.time())) def ota_generate(self, args, id=0): command = ['ota_from_target_files'] @@ -71,15 +189,28 @@ class ProcessesManagement: command.append(args['partial']) command.append(args['target']) command.append(args['output']) - # Start a subprocess and collect the output - stderr_pipes = pipes.Template() - stdout_pipes = pipes.Template() - ferr = stderr_pipes.open(os.path.join( - 'output', 'stderr.'+str(id)), 'w') - fout = stdout_pipes.open(os.path.join( - 'output', 'stdout.'+str(id)), 'w') - self.set(id, subprocess.Popen( - command, stderr=ferr, stdout=fout)) + job_info = JobInfo(id, + target=args['target'], + incremental=args['incremental'] if args['isIncremental'] else '', + verbose=args['verbose'], + partial=args['partial'].split( + ' ') if args['isPartial'] else [], + output=args['output'], + status='Running', + extra=args['extra'], + start_time=int(time.time()) + ) + try: + thread = threading.Thread(target=self.ota_run, args=(command, id)) + with sqlite3.connect(self.path) as connect: + cursor = connect.cursor() + cursor.execute(""" + INSERT INTO Jobs (ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime) + VALUES (:id, :target, :incremental, :verbose, :partial, :output, :status, :downgrade, :extra, :stdout, :stderr, :start_time) + """, job_info.to_sql_form_dict()) + thread.start() + except AssertionError: + raise SyntaxError logging.info( 'Starting generating OTA package with id {}: \n {}' - .format(id, command)) \ No newline at end of file + .format(id, command)) diff --git a/tools/otagui/src/components/JobConfiguration.vue b/tools/otagui/src/components/JobConfiguration.vue new file mode 100644 index 000000000..17e660637 --- /dev/null +++ b/tools/otagui/src/components/JobConfiguration.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/tools/otagui/src/components/JobDisplay.vue b/tools/otagui/src/components/JobDisplay.vue index a1f9eafdd..83a1aa812 100644 --- a/tools/otagui/src/components/JobDisplay.vue +++ b/tools/otagui/src/components/JobDisplay.vue @@ -1,26 +1,39 @@ - \ No newline at end of file diff --git a/tools/otagui/src/views/JobList.vue b/tools/otagui/src/views/JobList.vue index 9f58095a1..53450a944 100644 --- a/tools/otagui/src/views/JobList.vue +++ b/tools/otagui/src/views/JobList.vue @@ -4,6 +4,9 @@ v-for="job in jobs" :key="job.id" :job="job" + :active="overStatus.get(job.id)" + @mouseover="mouseOver(job.id, true)" + @mouseout="mouseOver(job.id, false)" />