diff --git a/tools/otagui/.dockerignore b/tools/otagui/.dockerignore index e3f4a471c..17ab9de80 100644 --- a/tools/otagui/.dockerignore +++ b/tools/otagui/.dockerignore @@ -1,2 +1,7 @@ node_modules/ -Dockerfile \ No newline at end of file +Dockerfile +.vscode +dist +output +target +*.db diff --git a/tools/otagui/.gitignore b/tools/otagui/.gitignore index 323330f74..1ad738ffb 100644 --- a/tools/otagui/.gitignore +++ b/tools/otagui/.gitignore @@ -28,3 +28,6 @@ pnpm-debug.log* *.sln *.sw? *.db + +packaged +**/.DS_Store diff --git a/tools/otagui/Dockerfile b/tools/otagui/Dockerfile index 5be13ef86..ad1b47b62 100644 --- a/tools/otagui/Dockerfile +++ b/tools/otagui/Dockerfile @@ -3,7 +3,12 @@ FROM node:lts-alpine as build-stage WORKDIR /app COPY package*.json ./ RUN npm install -COPY . . +COPY src ./src +COPY public ./public +COPY *.js . +COPY .env* . +COPY .eslint* . + RUN npm run build # production stage @@ -14,7 +19,7 @@ WORKDIR /app VOLUME [ "/app/target", "/app/output"] COPY otatools.zip . COPY --from=build-stage /app/dist ./dist -COPY --from=build-stage /app/*.py . +COPY *.py . EXPOSE 8000 CMD ["python3.9", "web_server.py"] \ No newline at end of file diff --git a/tools/otagui/build_zip.bash b/tools/otagui/build_zip.bash new file mode 100755 index 000000000..2cc68154c --- /dev/null +++ b/tools/otagui/build_zip.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +image_id=$(docker build -q .) +container_id=$(docker run -d --entrypoint /usr/bin/sleep ${image_id} 60) +docker container exec ${container_id} zip /app.zip -r /app +docker container cp ${container_id}:/app.zip . +docker container stop ${container_id} +docker container rm ${container_id} \ No newline at end of file diff --git a/tools/otagui/ota_interface.py b/tools/otagui/ota_interface.py index 127c1a73f..a4ca86872 100644 --- a/tools/otagui/ota_interface.py +++ b/tools/otagui/ota_interface.py @@ -1,6 +1,5 @@ import subprocess import os -import json import pipes import threading from dataclasses import dataclass, asdict, field @@ -31,16 +30,6 @@ class JobInfo: isIncremental: bool = False def __post_init__(self): - """ - If the output, stdout, stderr paths are not set, automatically use - the job id as the file name. - """ - 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( @@ -108,16 +97,43 @@ class JobInfo: return detail_info +class DependencyError(Exception): + pass + + class ProcessesManagement: """ A class manage the ota generate process """ - def __init__(self, path='output/ota_database.db'): + @staticmethod + def check_external_dependencies(): + try: + java_version = subprocess.check_output(["java", "--version"]) + print("Java version:", java_version.decode()) + except Exception as e: + raise DependencyError( + "java not found in PATH. Attempt to generate OTA might fail. " + str(e)) + try: + zip_version = subprocess.check_output(["zip", "-v"]) + print("Zip version:", zip_version.decode()) + except Exception as e: + raise DependencyError( + "zip command not found in PATH. Attempt to generate OTA might fail. " + str(e)) + + def __init__(self, *, working_dir='output', db_path=None, otatools_dir=None): """ create a table if not exist """ - self.path = path + ProcessesManagement.check_external_dependencies() + self.working_dir = working_dir + self.logs_dir = os.path.join(working_dir, 'logs') + self.otatools_dir = otatools_dir + os.makedirs(self.working_dir, exist_ok=True) + os.makedirs(self.logs_dir, exist_ok=True) + if not db_path: + db_path = os.path.join(self.working_dir, "ota_database.db") + self.path = db_path with sqlite3.connect(self.path) as connect: cursor = connect.cursor() cursor.execute(""" @@ -145,8 +161,8 @@ class ProcessesManagement: job_info: JobInfo """ with sqlite3.connect(self.path) as connect: - cursor = connect.cursor() - cursor.execute(""" + cursor = connect.cursor() + cursor.execute(""" INSERT INTO Jobs (ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, Finishtime) VALUES (:id, :target, :incremental, :verbose, :partial, :output, :status, :downgrade, :extra, :stdout, :stderr, :start_time, :finish_time) """, job_info.to_sql_form_dict()) @@ -165,7 +181,7 @@ class ProcessesManagement: cursor.execute(""" SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime FROM Jobs WHERE ID=(?) - """, (id,)) + """, (str(id),)) row = cursor.fetchone() status = JobInfo(*row) return status @@ -202,32 +218,41 @@ class ProcessesManagement: """, (status, finish_time, id)) - def ota_run(self, command, id): + def ota_run(self, command, id, stdout_path, stderr_path): """ Initiate a subprocess to run the ota generation. Wait until it finished and update the record in the database. """ stderr_pipes = pipes.Template() stdout_pipes = pipes.Template() + ferr = stderr_pipes.open(stdout_path, 'w') + fout = stdout_pipes.open(stderr_path, 'w') + env = {} + if self.otatools_dir: + env['PATH'] = os.path.join( + self.otatools_dir, "bin") + ":" + os.environ["PATH"] # TODO(lishutong): Enable user to use self-defined stderr/stdout path - 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, shell=False) + command, stderr=ferr, stdout=fout, shell=False, env=env, cwd=self.otatools_dir) except FileNotFoundError as e: logging.error('ota_from_target_files is not set properly %s', e) 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: + raise + except Exception as e: + logging.error('Failed to execute ota_from_target_files %s', e) self.update_status(id, 'Error', int(time.time())) + raise - def ota_generate(self, args, id=0): + def wait_result(): + 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())) + threading.Thread(target=wait_result).start() + + def ota_generate(self, args, id): """ Read in the arguments from the frontend and start running the OTA generation process, then update the records in database. @@ -244,40 +269,41 @@ class ProcessesManagement: if not os.path.isfile(args['target']): raise FileNotFoundError if not 'output' in args: - args['output'] = os.path.join('output', str(id) + '.zip') + args['output'] = os.path.join(self.working_dir, str(id) + '.zip') if args['verbose']: command.append('-v') if args['extra_keys']: - args['extra'] = \ - '--' + ' --'.join(args['extra_keys']) + ' ' + args['extra'] + args['extra'] = '--' + \ + ' --'.join(args['extra_keys']) + ' ' + args['extra'] if args['extra']: command += args['extra'].strip().split(' ') if args['isIncremental']: if not os.path.isfile(args['incremental']): raise FileNotFoundError command.append('-i') - command.append(args['incremental']) + command.append(os.path.realpath(args['incremental'])) if args['isPartial']: command.append('--partial') command.append(' '.join(args['partial'])) - command.append(args['target']) - command.append(args['output']) + command.append(os.path.realpath(args['target'])) + command.append(os.path.realpath(args['output'])) + stdout = os.path.join(self.logs_dir, 'stdout.' + str(id)) + stderr = os.path.join(self.logs_dir, 'stderr.' + str(id)) job_info = JobInfo(id, target=args['target'], incremental=args['incremental'] if args['isIncremental'] else '', verbose=args['verbose'], - partial=args['partial'] if args['isPartial'] else [], + partial=args['partial'] if args['isPartial'] else [ + ], output=args['output'], status='Running', extra=args['extra'], - start_time=int(time.time()) + start_time=int(time.time()), + stdout=stdout, + stderr=stderr ) - try: - thread = threading.Thread(target=self.ota_run, args=(command, id)) - self.insert_database(job_info) - thread.start() - except AssertionError: - raise SyntaxError + self.ota_run(command, id, job_info.stdout, job_info.stderr) + self.insert_database(job_info) logging.info( 'Starting generating OTA package with id {}: \n {}' .format(id, command)) diff --git a/tools/otagui/src/components/BatchOTAOptions.vue b/tools/otagui/src/components/BatchOTAOptions.vue index fcbc6e01b..8d1278f60 100644 --- a/tools/otagui/src/components/BatchOTAOptions.vue +++ b/tools/otagui/src/components/BatchOTAOptions.vue @@ -74,8 +74,9 @@ export default { */ async sendForm() { try { - let response_messages = await this.$store.state.otaConfig.sendForms( - this.targetBuilds, this.incrementalSources) + let response_data = await this.$store.state.otaConfig.sendForms( + this.targetBuilds, this.incrementalSources); + let response_messages = response_data.map(d => d.msg); alert(response_messages.join('\n')) this.$store.state.otaConfig.reset() this.$store.commit('SET_TARGETS', []) diff --git a/tools/otagui/src/components/BuildLibrary.vue b/tools/otagui/src/components/BuildLibrary.vue index 7f27732c9..bfada10be 100644 --- a/tools/otagui/src/components/BuildLibrary.vue +++ b/tools/otagui/src/components/BuildLibrary.vue @@ -3,7 +3,7 @@

Build Library

  • import UploadFile from '@/components/UploadFile.vue' import BuildTable from '@/components/BuildTable.vue' -import ApiService from '@/services/ApiService.js' +import ApiService from '../services/ApiService.js' import FormDate from '@/services/FormDate.js' export default { @@ -72,9 +72,8 @@ export default { */ async fetchTargetList() { try { - let response = await ApiService.getFileList('') - this.targetDetails = response.data - this.$emit('update:targetDetails', response.data) + this.targetDetails = await ApiService.getBuildList() + this.$emit('update:targetDetails', this.targetDetails) } catch (err) { alert( "Cannot fetch Android Builds list from the backend, for the following reasons:" @@ -88,9 +87,8 @@ export default { */ async updateBuildLib() { try { - let response = await ApiService.getFileList('/target') - this.targetDetails = response.data - this.$emit('update:targetDetails', response.data) + this.targetDetails = await ApiService.reconstructBuildList(); + this.$emit('update:targetDetails', this.targetDetails); } catch (err) { alert( "Cannot fetch Android Builds list from the backend, for the following reasons: " diff --git a/tools/otagui/src/components/ChainOTAOptions.vue b/tools/otagui/src/components/ChainOTAOptions.vue index 37d44f733..fb733fe61 100644 --- a/tools/otagui/src/components/ChainOTAOptions.vue +++ b/tools/otagui/src/components/ChainOTAOptions.vue @@ -74,12 +74,13 @@ export default { if (this.targetBuilds.length<2) { alert( 'At least two OTA packeges has to be given!' - ) + ); return } try { - let response_messages = await this.$store.state.otaConfig + let response_data = await this.$store.state.otaConfig .sendChainForms(this.targetBuilds) + let response_messages = response_data.map(d => d.msg); alert(response_messages.join('\n')) this.$store.state.otaConfig.reset() this.$store.commit('SET_TARGETS', []) diff --git a/tools/otagui/src/components/SingleOTAOptions.vue b/tools/otagui/src/components/SingleOTAOptions.vue index 02d6b84ce..3123f6c84 100644 --- a/tools/otagui/src/components/SingleOTAOptions.vue +++ b/tools/otagui/src/components/SingleOTAOptions.vue @@ -74,9 +74,9 @@ export default { */ async sendForm() { try { - let response_message = await this.$store.state.otaConfig.sendForm( + let data = await this.$store.state.otaConfig.sendForm( this.targetBuild, this.incrementalSource) - alert(response_message) + alert(data.msg); this.$store.state.otaConfig.reset() this.$store.commit('SET_TARGETS', []) this.$store.commit('SET_SOURCES', []) diff --git a/tools/otagui/src/services/ApiService.js b/tools/otagui/src/services/ApiService.js index b79abf68b..84140c6f9 100644 --- a/tools/otagui/src/services/ApiService.js +++ b/tools/otagui/src/services/ApiService.js @@ -1,6 +1,6 @@ import axios from 'axios' -const baseURL = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:8000'; +const baseURL = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:5000'; console.log(`Build mode: ${process.env.NODE_ENV}, API base url ${baseURL}`); @@ -23,8 +23,13 @@ export default { getJobById(id) { return apiClient.get("/check/" + id) }, - getFileList(path) { - return apiClient.get("/file" + path) + async getBuildList() { + let resp = await apiClient.get("/file"); + return resp.data || []; + }, + async reconstructBuildList() { + let resp = await apiClient.get("/reconstruct_build_list"); + return resp.data; }, uploadTarget(file, onUploadProgress) { let formData = new FormData() @@ -37,12 +42,15 @@ export default { }, async postInput(input, id) { try { - const response = await apiClient.post( - '/run/' + id, input) - return response - } catch (err) { - console.log('err:', err) - return + let resp = await apiClient.post( + '/run/' + id, JSON.stringify(input)); + return resp.data; + } catch (error) { + if (error.response.data) { + return error.response.data; + } else { + throw error; + } } } -} +} \ No newline at end of file diff --git a/tools/otagui/src/services/JobSubmission.js b/tools/otagui/src/services/JobSubmission.js index e65661bd7..ddf5a4489 100644 --- a/tools/otagui/src/services/JobSubmission.js +++ b/tools/otagui/src/services/JobSubmission.js @@ -89,6 +89,7 @@ export class OTAConfiguration { let jsonOptions = Object.assign({}, this) jsonOptions.target = targetBuild jsonOptions.incremental = incrementalSource + jsonOptions.isIncremental = !!incrementalSource; jsonOptions.id = uuid.v1() for (let flag of OTAExtraFlags) { if (jsonOptions[flag.key]) { @@ -97,8 +98,8 @@ export class OTAConfiguration { } } } - let response = await ApiServices.postInput(jsonOptions, jsonOptions.id) - return response.data + let data = await ApiServices.postInput(jsonOptions, jsonOptions.id) + return data; } /** diff --git a/tools/otagui/src/views/JobDetails.vue b/tools/otagui/src/views/JobDetails.vue index 56b6e55fc..7bb7a3daf 100644 --- a/tools/otagui/src/views/JobDetails.vue +++ b/tools/otagui/src/views/JobDetails.vue @@ -16,21 +16,21 @@

    STDERR

    -
    {{ job.stderr }}

    -

    +

    STDOUT

    -
    {{ job.stdout }}

    -

    +
    diff --git a/tools/otagui/src/views/JobList.vue b/tools/otagui/src/views/JobList.vue index 0dd96cefd..1db5d5755 100644 --- a/tools/otagui/src/views/JobList.vue +++ b/tools/otagui/src/views/JobList.vue @@ -3,21 +3,6 @@ v-if="jobs" :jobs="jobs" /> - - - - -