Merge changes Ic5fdefe2,I32cbd027,I464d846c,Ie9ef8e2b,Ib7b02d5c am: e6ae7c629b am: 8834603a01
Original change: https://android-review.googlesource.com/c/platform/development/+/1815621 Change-Id: Ifa73b12a346a8640c29a5891e6634f6d3085225d
This commit is contained in:
@@ -1,2 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
.vscode
|
||||||
|
dist
|
||||||
|
output
|
||||||
|
target
|
||||||
|
*.db
|
||||||
|
|||||||
3
tools/otagui/.gitignore
vendored
3
tools/otagui/.gitignore
vendored
@@ -28,3 +28,6 @@ pnpm-debug.log*
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
packaged
|
||||||
|
**/.DS_Store
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ FROM node:lts-alpine as build-stage
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY src ./src
|
||||||
|
COPY public ./public
|
||||||
|
COPY *.js .
|
||||||
|
COPY .env* .
|
||||||
|
COPY .eslint* .
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
@@ -14,7 +19,7 @@ WORKDIR /app
|
|||||||
VOLUME [ "/app/target", "/app/output"]
|
VOLUME [ "/app/target", "/app/output"]
|
||||||
COPY otatools.zip .
|
COPY otatools.zip .
|
||||||
COPY --from=build-stage /app/dist ./dist
|
COPY --from=build-stage /app/dist ./dist
|
||||||
COPY --from=build-stage /app/*.py .
|
COPY *.py .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD ["python3.9", "web_server.py"]
|
CMD ["python3.9", "web_server.py"]
|
||||||
8
tools/otagui/build_zip.bash
Executable file
8
tools/otagui/build_zip.bash
Executable file
@@ -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}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import pipes
|
import pipes
|
||||||
import threading
|
import threading
|
||||||
from dataclasses import dataclass, asdict, field
|
from dataclasses import dataclass, asdict, field
|
||||||
@@ -31,16 +30,6 @@ class JobInfo:
|
|||||||
isIncremental: bool = False
|
isIncremental: bool = False
|
||||||
|
|
||||||
def __post_init__(self):
|
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)
|
def enforce_bool(t): return t if isinstance(t, bool) else bool(t)
|
||||||
self.verbose, self.downgrade = map(
|
self.verbose, self.downgrade = map(
|
||||||
@@ -108,16 +97,43 @@ class JobInfo:
|
|||||||
return detail_info
|
return detail_info
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ProcessesManagement:
|
class ProcessesManagement:
|
||||||
"""
|
"""
|
||||||
A class manage the ota generate process
|
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
|
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:
|
with sqlite3.connect(self.path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@@ -145,8 +161,8 @@ class ProcessesManagement:
|
|||||||
job_info: JobInfo
|
job_info: JobInfo
|
||||||
"""
|
"""
|
||||||
with sqlite3.connect(self.path) as connect:
|
with sqlite3.connect(self.path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO Jobs (ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, Finishtime)
|
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)
|
VALUES (:id, :target, :incremental, :verbose, :partial, :output, :status, :downgrade, :extra, :stdout, :stderr, :start_time, :finish_time)
|
||||||
""", job_info.to_sql_form_dict())
|
""", job_info.to_sql_form_dict())
|
||||||
@@ -165,7 +181,7 @@ class ProcessesManagement:
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime
|
SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime
|
||||||
FROM Jobs WHERE ID=(?)
|
FROM Jobs WHERE ID=(?)
|
||||||
""", (id,))
|
""", (str(id),))
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
status = JobInfo(*row)
|
status = JobInfo(*row)
|
||||||
return status
|
return status
|
||||||
@@ -202,32 +218,41 @@ class ProcessesManagement:
|
|||||||
""",
|
""",
|
||||||
(status, finish_time, id))
|
(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
|
Initiate a subprocess to run the ota generation. Wait until it finished and update
|
||||||
the record in the database.
|
the record in the database.
|
||||||
"""
|
"""
|
||||||
stderr_pipes = pipes.Template()
|
stderr_pipes = pipes.Template()
|
||||||
stdout_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
|
# 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:
|
try:
|
||||||
proc = subprocess.Popen(
|
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:
|
except FileNotFoundError as e:
|
||||||
logging.error('ota_from_target_files is not set properly %s', e)
|
logging.error('ota_from_target_files is not set properly %s', e)
|
||||||
self.update_status(id, 'Error', int(time.time()))
|
self.update_status(id, 'Error', int(time.time()))
|
||||||
return
|
raise
|
||||||
exit_code = proc.wait()
|
except Exception as e:
|
||||||
if exit_code == 0:
|
logging.error('Failed to execute ota_from_target_files %s', e)
|
||||||
self.update_status(id, 'Finished', int(time.time()))
|
|
||||||
else:
|
|
||||||
self.update_status(id, 'Error', int(time.time()))
|
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
|
Read in the arguments from the frontend and start running the OTA
|
||||||
generation process, then update the records in database.
|
generation process, then update the records in database.
|
||||||
@@ -244,40 +269,41 @@ class ProcessesManagement:
|
|||||||
if not os.path.isfile(args['target']):
|
if not os.path.isfile(args['target']):
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
if not 'output' in args:
|
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']:
|
if args['verbose']:
|
||||||
command.append('-v')
|
command.append('-v')
|
||||||
if args['extra_keys']:
|
if args['extra_keys']:
|
||||||
args['extra'] = \
|
args['extra'] = '--' + \
|
||||||
'--' + ' --'.join(args['extra_keys']) + ' ' + args['extra']
|
' --'.join(args['extra_keys']) + ' ' + args['extra']
|
||||||
if args['extra']:
|
if args['extra']:
|
||||||
command += args['extra'].strip().split(' ')
|
command += args['extra'].strip().split(' ')
|
||||||
if args['isIncremental']:
|
if args['isIncremental']:
|
||||||
if not os.path.isfile(args['incremental']):
|
if not os.path.isfile(args['incremental']):
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
command.append('-i')
|
command.append('-i')
|
||||||
command.append(args['incremental'])
|
command.append(os.path.realpath(args['incremental']))
|
||||||
if args['isPartial']:
|
if args['isPartial']:
|
||||||
command.append('--partial')
|
command.append('--partial')
|
||||||
command.append(' '.join(args['partial']))
|
command.append(' '.join(args['partial']))
|
||||||
command.append(args['target'])
|
command.append(os.path.realpath(args['target']))
|
||||||
command.append(args['output'])
|
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,
|
job_info = JobInfo(id,
|
||||||
target=args['target'],
|
target=args['target'],
|
||||||
incremental=args['incremental'] if args['isIncremental'] else '',
|
incremental=args['incremental'] if args['isIncremental'] else '',
|
||||||
verbose=args['verbose'],
|
verbose=args['verbose'],
|
||||||
partial=args['partial'] if args['isPartial'] else [],
|
partial=args['partial'] if args['isPartial'] else [
|
||||||
|
],
|
||||||
output=args['output'],
|
output=args['output'],
|
||||||
status='Running',
|
status='Running',
|
||||||
extra=args['extra'],
|
extra=args['extra'],
|
||||||
start_time=int(time.time())
|
start_time=int(time.time()),
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr
|
||||||
)
|
)
|
||||||
try:
|
self.ota_run(command, id, job_info.stdout, job_info.stderr)
|
||||||
thread = threading.Thread(target=self.ota_run, args=(command, id))
|
self.insert_database(job_info)
|
||||||
self.insert_database(job_info)
|
|
||||||
thread.start()
|
|
||||||
except AssertionError:
|
|
||||||
raise SyntaxError
|
|
||||||
logging.info(
|
logging.info(
|
||||||
'Starting generating OTA package with id {}: \n {}'
|
'Starting generating OTA package with id {}: \n {}'
|
||||||
.format(id, command))
|
.format(id, command))
|
||||||
|
|||||||
@@ -74,8 +74,9 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async sendForm() {
|
async sendForm() {
|
||||||
try {
|
try {
|
||||||
let response_messages = await this.$store.state.otaConfig.sendForms(
|
let response_data = await this.$store.state.otaConfig.sendForms(
|
||||||
this.targetBuilds, this.incrementalSources)
|
this.targetBuilds, this.incrementalSources);
|
||||||
|
let response_messages = response_data.map(d => d.msg);
|
||||||
alert(response_messages.join('\n'))
|
alert(response_messages.join('\n'))
|
||||||
this.$store.state.otaConfig.reset()
|
this.$store.state.otaConfig.reset()
|
||||||
this.$store.commit('SET_TARGETS', [])
|
this.$store.commit('SET_TARGETS', [])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h3>Build Library</h3>
|
<h3>Build Library</h3>
|
||||||
<UploadFile @file-uploaded="fetchTargetList" />
|
<UploadFile @file-uploaded="fetchTargetList" />
|
||||||
<BuildTable
|
<BuildTable
|
||||||
v-if="targetDetails.length>0"
|
v-if="targetDetails && targetDetails.length>0"
|
||||||
:builds="targetDetails"
|
:builds="targetDetails"
|
||||||
/>
|
/>
|
||||||
<li
|
<li
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import UploadFile from '@/components/UploadFile.vue'
|
import UploadFile from '@/components/UploadFile.vue'
|
||||||
import BuildTable from '@/components/BuildTable.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'
|
import FormDate from '@/services/FormDate.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -72,9 +72,8 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async fetchTargetList() {
|
async fetchTargetList() {
|
||||||
try {
|
try {
|
||||||
let response = await ApiService.getFileList('')
|
this.targetDetails = await ApiService.getBuildList()
|
||||||
this.targetDetails = response.data
|
this.$emit('update:targetDetails', this.targetDetails)
|
||||||
this.$emit('update:targetDetails', response.data)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(
|
alert(
|
||||||
"Cannot fetch Android Builds list from the backend, for the following reasons:"
|
"Cannot fetch Android Builds list from the backend, for the following reasons:"
|
||||||
@@ -88,9 +87,8 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async updateBuildLib() {
|
async updateBuildLib() {
|
||||||
try {
|
try {
|
||||||
let response = await ApiService.getFileList('/target')
|
this.targetDetails = await ApiService.reconstructBuildList();
|
||||||
this.targetDetails = response.data
|
this.$emit('update:targetDetails', this.targetDetails);
|
||||||
this.$emit('update:targetDetails', response.data)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(
|
alert(
|
||||||
"Cannot fetch Android Builds list from the backend, for the following reasons: "
|
"Cannot fetch Android Builds list from the backend, for the following reasons: "
|
||||||
|
|||||||
@@ -74,12 +74,13 @@ export default {
|
|||||||
if (this.targetBuilds.length<2) {
|
if (this.targetBuilds.length<2) {
|
||||||
alert(
|
alert(
|
||||||
'At least two OTA packeges has to be given!'
|
'At least two OTA packeges has to be given!'
|
||||||
)
|
);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let response_messages = await this.$store.state.otaConfig
|
let response_data = await this.$store.state.otaConfig
|
||||||
.sendChainForms(this.targetBuilds)
|
.sendChainForms(this.targetBuilds)
|
||||||
|
let response_messages = response_data.map(d => d.msg);
|
||||||
alert(response_messages.join('\n'))
|
alert(response_messages.join('\n'))
|
||||||
this.$store.state.otaConfig.reset()
|
this.$store.state.otaConfig.reset()
|
||||||
this.$store.commit('SET_TARGETS', [])
|
this.$store.commit('SET_TARGETS', [])
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async sendForm() {
|
async sendForm() {
|
||||||
try {
|
try {
|
||||||
let response_message = await this.$store.state.otaConfig.sendForm(
|
let data = await this.$store.state.otaConfig.sendForm(
|
||||||
this.targetBuild, this.incrementalSource)
|
this.targetBuild, this.incrementalSource)
|
||||||
alert(response_message)
|
alert(data.msg);
|
||||||
this.$store.state.otaConfig.reset()
|
this.$store.state.otaConfig.reset()
|
||||||
this.$store.commit('SET_TARGETS', [])
|
this.$store.commit('SET_TARGETS', [])
|
||||||
this.$store.commit('SET_SOURCES', [])
|
this.$store.commit('SET_SOURCES', [])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios'
|
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}`);
|
console.log(`Build mode: ${process.env.NODE_ENV}, API base url ${baseURL}`);
|
||||||
|
|
||||||
@@ -23,8 +23,13 @@ export default {
|
|||||||
getJobById(id) {
|
getJobById(id) {
|
||||||
return apiClient.get("/check/" + id)
|
return apiClient.get("/check/" + id)
|
||||||
},
|
},
|
||||||
getFileList(path) {
|
async getBuildList() {
|
||||||
return apiClient.get("/file" + path)
|
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) {
|
uploadTarget(file, onUploadProgress) {
|
||||||
let formData = new FormData()
|
let formData = new FormData()
|
||||||
@@ -37,12 +42,15 @@ export default {
|
|||||||
},
|
},
|
||||||
async postInput(input, id) {
|
async postInput(input, id) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(
|
let resp = await apiClient.post(
|
||||||
'/run/' + id, input)
|
'/run/' + id, JSON.stringify(input));
|
||||||
return response
|
return resp.data;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.log('err:', err)
|
if (error.response.data) {
|
||||||
return
|
return error.response.data;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +89,7 @@ export class OTAConfiguration {
|
|||||||
let jsonOptions = Object.assign({}, this)
|
let jsonOptions = Object.assign({}, this)
|
||||||
jsonOptions.target = targetBuild
|
jsonOptions.target = targetBuild
|
||||||
jsonOptions.incremental = incrementalSource
|
jsonOptions.incremental = incrementalSource
|
||||||
|
jsonOptions.isIncremental = !!incrementalSource;
|
||||||
jsonOptions.id = uuid.v1()
|
jsonOptions.id = uuid.v1()
|
||||||
for (let flag of OTAExtraFlags) {
|
for (let flag of OTAExtraFlags) {
|
||||||
if (jsonOptions[flag.key]) {
|
if (jsonOptions[flag.key]) {
|
||||||
@@ -97,8 +98,8 @@ export class OTAConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let response = await ApiServices.postInput(jsonOptions, jsonOptions.id)
|
let data = await ApiServices.postInput(jsonOptions, jsonOptions.id)
|
||||||
return response.data
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,21 +16,21 @@
|
|||||||
<v-divider class="my-5" />
|
<v-divider class="my-5" />
|
||||||
<div>
|
<div>
|
||||||
<h3>STDERR</h3>
|
<h3>STDERR</h3>
|
||||||
<div
|
<pre
|
||||||
ref="stderr"
|
ref="stderr"
|
||||||
class="stderr"
|
class="stderr"
|
||||||
>
|
>
|
||||||
{{ job.stderr }}
|
{{ job.stderr }}
|
||||||
<p ref="stderrBottom" />
|
<p ref="stderrBottom" />
|
||||||
</div>
|
</pre>
|
||||||
<h3>STDOUT</h3>
|
<h3>STDOUT</h3>
|
||||||
<div
|
<pre
|
||||||
ref="stdout"
|
ref="stdout"
|
||||||
class="stdout"
|
class="stdout"
|
||||||
>
|
>
|
||||||
{{ job.stdout }}
|
{{ job.stdout }}
|
||||||
<p ref="stdoutBottom" />
|
<p ref="stdoutBottom" />
|
||||||
</div>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<v-divider class="my-5" />
|
<v-divider class="my-5" />
|
||||||
<div class="download">
|
<div class="download">
|
||||||
|
|||||||
@@ -3,21 +3,6 @@
|
|||||||
v-if="jobs"
|
v-if="jobs"
|
||||||
:jobs="jobs"
|
:jobs="jobs"
|
||||||
/>
|
/>
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="job in jobs"
|
|
||||||
:key="job.id"
|
|
||||||
cols="12"
|
|
||||||
sm="3"
|
|
||||||
>
|
|
||||||
<JobDisplay
|
|
||||||
:job="job"
|
|
||||||
:active="overStatus.get(job.id)"
|
|
||||||
@mouseover="mouseOver(job.id, true)"
|
|
||||||
@mouseout="mouseOver(job.id, false)"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
@click="updateStatus"
|
@click="updateStatus"
|
||||||
@@ -27,20 +12,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import JobDisplay from '@/components/JobDisplay.vue'
|
|
||||||
import ApiService from '../services/ApiService.js'
|
import ApiService from '../services/ApiService.js'
|
||||||
import OTAJobTable from '@/components/OTAJobTable.vue'
|
import OTAJobTable from '@/components/OTAJobTable.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'JobList',
|
name: 'JobList',
|
||||||
components: {
|
components: {
|
||||||
JobDisplay,
|
|
||||||
OTAJobTable
|
OTAJobTable
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
jobs: null,
|
jobs: null,
|
||||||
overStatus: new Map()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created (){
|
created (){
|
||||||
@@ -55,9 +37,6 @@ export default {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mouseOver(id, status) {
|
|
||||||
this.overStatus.set(id, status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import re
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class BuildFileInvalidError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BuildInfo:
|
class BuildInfo:
|
||||||
"""
|
"""
|
||||||
@@ -34,29 +38,26 @@ class BuildInfo:
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
build = zipfile.ZipFile(self.path)
|
with zipfile.ZipFile(self.path) as build:
|
||||||
try:
|
try:
|
||||||
with build.open('SYSTEM/build.prop', 'r') as build_prop:
|
with build.open('SYSTEM/build.prop', 'r') as build_prop:
|
||||||
raw_info = build_prop.readlines()
|
raw_info = build_prop.readlines()
|
||||||
pattern_id = re.compile(b'(?<=ro\.build\.id\=).+')
|
pattern_id = re.compile(b'(?<=ro\.build\.id\=).+')
|
||||||
pattern_version = re.compile(
|
pattern_version = re.compile(
|
||||||
b'(?<=ro\.build\.version\.incremental\=).+')
|
b'(?<=ro\.build\.version\.incremental\=).+')
|
||||||
pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+')
|
pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+')
|
||||||
self.build_id = extract_info(
|
self.build_id = extract_info(
|
||||||
pattern_id, raw_info).decode('utf-8')
|
pattern_id, raw_info).decode('utf-8')
|
||||||
self.build_version = extract_info(
|
self.build_version = extract_info(
|
||||||
pattern_version, raw_info).decode('utf-8')
|
pattern_version, raw_info).decode('utf-8')
|
||||||
self.build_flavor = extract_info(
|
self.build_flavor = extract_info(
|
||||||
pattern_flavor, raw_info).decode('utf-8')
|
pattern_flavor, raw_info).decode('utf-8')
|
||||||
except KeyError:
|
with build.open('META/ab_partitions.txt', 'r') as partition_info:
|
||||||
pass
|
raw_info = partition_info.readlines()
|
||||||
try:
|
for line in raw_info:
|
||||||
with build.open('META/ab_partitions.txt', 'r') as partition_info:
|
self.partitions.append(line.decode('utf-8').rstrip())
|
||||||
raw_info = partition_info.readlines()
|
except KeyError as e:
|
||||||
for line in raw_info:
|
raise BuildFileInvalidError("Invalid build due to " + str(e))
|
||||||
self.partitions.append(line.decode('utf-8').rstrip())
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def to_sql_form_dict(self):
|
def to_sql_form_dict(self):
|
||||||
"""
|
"""
|
||||||
@@ -79,12 +80,16 @@ class TargetLib:
|
|||||||
"""
|
"""
|
||||||
A class that manages the builds in database.
|
A class that manages the builds in database.
|
||||||
"""
|
"""
|
||||||
def __init__(self, path='target/ota_database.db'):
|
|
||||||
|
def __init__(self, working_dir="target", db_path=None):
|
||||||
"""
|
"""
|
||||||
Create a build table if not existing
|
Create a build table if not existing
|
||||||
"""
|
"""
|
||||||
self.path = path
|
self.working_dir = working_dir
|
||||||
with sqlite3.connect(self.path) as connect:
|
if db_path is None:
|
||||||
|
db_path = os.path.join(working_dir, "ota_database.db")
|
||||||
|
self.db_path = db_path
|
||||||
|
with sqlite3.connect(self.db_path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE if not exists Builds (
|
CREATE TABLE if not exists Builds (
|
||||||
@@ -107,7 +112,12 @@ class TargetLib:
|
|||||||
"""
|
"""
|
||||||
build_info = BuildInfo(filename, path, int(time.time()))
|
build_info = BuildInfo(filename, path, int(time.time()))
|
||||||
build_info.analyse_buildprop()
|
build_info.analyse_buildprop()
|
||||||
with sqlite3.connect(self.path) as connect:
|
# Ignore name specified by user, instead use a standard format
|
||||||
|
build_info.path = os.path.join(self.working_dir, "{}-{}-{}.zip".format(
|
||||||
|
build_info.build_flavor, build_info.build_id, build_info.build_version))
|
||||||
|
if path != build_info.path:
|
||||||
|
os.rename(path, build_info.path)
|
||||||
|
with sqlite3.connect(self.db_path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT * FROM Builds WHERE FileName=:file_name and Path=:path
|
SELECT * FROM Builds WHERE FileName=:file_name and Path=:path
|
||||||
@@ -121,19 +131,21 @@ class TargetLib:
|
|||||||
VALUES (:file_name, :time, :path, :build_id, :build_version, :build_flavor, :partitions)
|
VALUES (:file_name, :time, :path, :build_id, :build_version, :build_flavor, :partitions)
|
||||||
""", build_info.to_sql_form_dict())
|
""", build_info.to_sql_form_dict())
|
||||||
|
|
||||||
def new_build_from_dir(self, path):
|
def new_build_from_dir(self):
|
||||||
"""
|
"""
|
||||||
Update the database using files under a directory
|
Update the database using files under a directory
|
||||||
Args:
|
Args:
|
||||||
path: a directory
|
path: a directory
|
||||||
"""
|
"""
|
||||||
if os.path.isdir(path):
|
build_dir = self.working_dir
|
||||||
builds_name = os.listdir(path)
|
if os.path.isdir(build_dir):
|
||||||
|
builds_name = os.listdir(build_dir)
|
||||||
for build_name in builds_name:
|
for build_name in builds_name:
|
||||||
if build_name.endswith(".zip"):
|
path = os.path.join(build_dir, build_name)
|
||||||
self.new_build(build_name, os.path.join(path, build_name))
|
if build_name.endswith(".zip") and zipfile.is_zipfile(path):
|
||||||
elif os.path.isfile(path) and path.endswith(".zip"):
|
self.new_build(build_name, path)
|
||||||
self.new_build(os.path.split(path)[-1], path)
|
elif os.path.isfile(build_dir) and build_dir.endswith(".zip"):
|
||||||
|
self.new_build(os.path.split(build_dir)[-1], build_dir)
|
||||||
return self.get_builds()
|
return self.get_builds()
|
||||||
|
|
||||||
def sql_to_buildinfo(self, row):
|
def sql_to_buildinfo(self, row):
|
||||||
@@ -147,7 +159,7 @@ class TargetLib:
|
|||||||
A list of build_info, each of which is an object:
|
A list of build_info, each of which is an object:
|
||||||
(FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
|
(FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
|
||||||
"""
|
"""
|
||||||
with sqlite3.connect(self.path) as connect:
|
with sqlite3.connect(self.db_path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
|
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
|
||||||
@@ -161,7 +173,7 @@ class TargetLib:
|
|||||||
A build_info, which is an object:
|
A build_info, which is an object:
|
||||||
(FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
|
(FileName, UploadTime, Path, Build ID, Build Version, Build Flavor, Partitions)
|
||||||
"""
|
"""
|
||||||
with sqlite3.connect(self.path) as connect:
|
with sqlite3.connect(self.db_path) as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
|
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class TestProcessesManagement(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
if os.path.isfile('test_process.db'):
|
if os.path.isfile('test_process.db'):
|
||||||
self.tearDown()
|
self.tearDown()
|
||||||
self.processes = ProcessesManagement(path='test_process.db')
|
self.processes = ProcessesManagement(db_path='test_process.db')
|
||||||
testcase_job_info = TestJobInfo()
|
testcase_job_info = TestJobInfo()
|
||||||
testcase_job_info.setUp()
|
testcase_job_info.setUp()
|
||||||
self.test_job_info = testcase_job_info.setup_job(incremental='target/source.zip')
|
self.test_job_info = testcase_job_info.setup_job(incremental='target/source.zip')
|
||||||
|
|||||||
12
tools/otagui/vue.config.js
Normal file
12
tools/otagui/vue.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
configureWebpack: {
|
||||||
|
resolve: {
|
||||||
|
symlinks: false,
|
||||||
|
alias: {
|
||||||
|
vue: path.resolve('./node_modules/vue')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@ import json
|
|||||||
import cgi
|
import cgi
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
import sys
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
LOCAL_ADDRESS = '0.0.0.0'
|
LOCAL_ADDRESS = '0.0.0.0'
|
||||||
@@ -78,17 +77,17 @@ class RequestHandler(CORSSimpleHTTPHandler):
|
|||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
json.dumps(status.to_dict_detail(target_lib)).encode()
|
json.dumps(status.to_dict_detail(target_lib)).encode()
|
||||||
)
|
)
|
||||||
elif self.path.startswith('/file'):
|
elif self.path.startswith('/file') or self.path.startswith("/reconstruct_build_list"):
|
||||||
if self.path == '/file' or self.path == '/file/':
|
if self.path == '/file' or self.path == '/file/':
|
||||||
file_list = target_lib.get_builds()
|
file_list = target_lib.get_builds()
|
||||||
else:
|
else:
|
||||||
file_list = target_lib.new_build_from_dir(self.path[6:])
|
file_list = target_lib.new_build_from_dir()
|
||||||
builds_info = [build.to_dict() for build in file_list]
|
builds_info = [build.to_dict() for build in file_list]
|
||||||
self._set_response(type='application/json')
|
self._set_response(type='application/json')
|
||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
json.dumps(builds_info).encode()
|
json.dumps(builds_info).encode()
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.debug(
|
||||||
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
||||||
str(self.path), str(self.headers), file_list
|
str(self.path), str(self.headers), file_list
|
||||||
)
|
)
|
||||||
@@ -115,12 +114,16 @@ class RequestHandler(CORSSimpleHTTPHandler):
|
|||||||
post_data = json.loads(self.rfile.read(content_length))
|
post_data = json.loads(self.rfile.read(content_length))
|
||||||
try:
|
try:
|
||||||
jobs.ota_generate(post_data, id=str(self.path[5:]))
|
jobs.ota_generate(post_data, id=str(self.path[5:]))
|
||||||
self._set_response(code=201)
|
self._set_response(code=200)
|
||||||
self.wfile.write(
|
self.send_header("Content-Type", 'application/json')
|
||||||
"ota generator start running".encode('utf-8'))
|
self.wfile.write(json.dumps(
|
||||||
except SyntaxError:
|
{"success": True, "msg": "OTA Generator started running"}).encode())
|
||||||
self.send_error(400)
|
except Exception as e:
|
||||||
logging.info(
|
logging.warning(
|
||||||
|
"Failed to run ota_from_target_files %s", e.__traceback__)
|
||||||
|
self.send_error(
|
||||||
|
400, "Failed to run ota_from_target_files", str(e))
|
||||||
|
logging.debug(
|
||||||
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
||||||
str(self.path), str(self.headers),
|
str(self.path), str(self.headers),
|
||||||
json.dumps(post_data)
|
json.dumps(post_data)
|
||||||
@@ -144,7 +147,8 @@ class RequestHandler(CORSSimpleHTTPHandler):
|
|||||||
file_length -= len(self.rfile.readline())
|
file_length -= len(self.rfile.readline())
|
||||||
BUFFER_SIZE = 1024*1024
|
BUFFER_SIZE = 1024*1024
|
||||||
for offset in range(0, file_length, BUFFER_SIZE):
|
for offset in range(0, file_length, BUFFER_SIZE):
|
||||||
chunk = self.rfile.read(min(file_length-offset, BUFFER_SIZE))
|
chunk = self.rfile.read(
|
||||||
|
min(file_length-offset, BUFFER_SIZE))
|
||||||
output_file.write(chunk)
|
output_file.write(chunk)
|
||||||
target_lib.new_build(self.path[6:], file_name)
|
target_lib.new_build(self.path[6:], file_name)
|
||||||
self._set_response(code=201)
|
self._set_response(code=201)
|
||||||
@@ -171,17 +175,17 @@ def run_server(SeverClass=ThreadedHTTPServer, HandlerClass=RequestHandler, port=
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
server_instance.server_close()
|
server_instance.server_close()
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
logging.info('Server has been turned off.')
|
logging.info('Server has been turned off.')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from sys import argv
|
from sys import argv
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
print(argv)
|
print(argv)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
EXTRACT_DIR = None
|
||||||
if os.path.exists("otatools.zip"):
|
if os.path.exists("otatools.zip"):
|
||||||
logging.info("Found otatools.zip, extracting...")
|
logging.info("Found otatools.zip, extracting...")
|
||||||
EXTRACT_DIR = "./"
|
EXTRACT_DIR = "/tmp/otatools-" + str(os.getpid())
|
||||||
os.makedirs(EXTRACT_DIR, exist_ok=True)
|
os.makedirs(EXTRACT_DIR, exist_ok=True)
|
||||||
with zipfile.ZipFile("otatools.zip", "r") as zfp:
|
with zipfile.ZipFile("otatools.zip", "r") as zfp:
|
||||||
zfp.extractall(EXTRACT_DIR)
|
zfp.extractall(EXTRACT_DIR)
|
||||||
@@ -189,19 +193,14 @@ if __name__ == '__main__':
|
|||||||
bin_dir = os.path.join(EXTRACT_DIR, "bin")
|
bin_dir = os.path.join(EXTRACT_DIR, "bin")
|
||||||
for filename in os.listdir(bin_dir):
|
for filename in os.listdir(bin_dir):
|
||||||
os.chmod(os.path.join(bin_dir, filename), stat.S_IRWXU)
|
os.chmod(os.path.join(bin_dir, filename), stat.S_IRWXU)
|
||||||
os.environ["PATH"] = os.path.join(EXTRACT_DIR, "bin") + ":" + os.environ["PATH"]
|
|
||||||
logging.info("Extracted otatools to {}".format(EXTRACT_DIR))
|
logging.info("Extracted otatools to {}".format(EXTRACT_DIR))
|
||||||
logging.info("PATH: %s", os.environ["PATH"])
|
|
||||||
if not os.path.isdir('target'):
|
if not os.path.isdir('target'):
|
||||||
os.mkdir('target', 755)
|
os.mkdir('target', 755)
|
||||||
if not os.path.isdir('output'):
|
if not os.path.isdir('output'):
|
||||||
os.mkdir('output', 755)
|
os.mkdir('output', 755)
|
||||||
target_lib = TargetLib()
|
target_lib = TargetLib()
|
||||||
jobs = ProcessesManagement()
|
jobs = ProcessesManagement(otatools_dir=EXTRACT_DIR)
|
||||||
try:
|
if len(argv) == 2:
|
||||||
if len(argv) == 2:
|
run_server(port=int(argv[1]))
|
||||||
run_server(port=int(argv[1]))
|
else:
|
||||||
else:
|
run_server()
|
||||||
run_server()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user