Merge "Use database to store ota generate history." am: f051c6f342 am: 692e5f1f06

Original change: https://android-review.googlesource.com/c/platform/development/+/1736940

Change-Id: I7f8f731ef05504fafa2511b71f57f02e2a3214a4
This commit is contained in:
Treehugger Robot
2021-06-21 23:37:03 +00:00
committed by Automerger Merge Worker
9 changed files with 332 additions and 90 deletions

View File

@@ -2,52 +2,170 @@ import subprocess
import os import os
import json import json
import pipes import pipes
from threading import Lock import threading
from dataclasses import dataclass, asdict, field
import logging 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: class ProcessesManagement:
def __init__(self): """
self.__container = {} A class manage the ota generate process
self.__lock = Lock() """
def set(self, name, value): def __init__(self, path='ota_database.db'):
with self.__lock: """
self.__container[name] = value 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): def get_status_by_ID(self, id):
with self.__lock: with sqlite3.connect(self.path) as connect:
return self.__container[name] cursor = connect.cursor()
logging.info(id)
def get_keys(self): cursor.execute("""
with self.__lock: SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime
return self.__container.keys() FROM Jobs WHERE ID=(?)
""", (id,))
def get_status_by_ID(self, id=0, details=False): row = cursor.fetchone()
status = {} status = JobInfo(*row)
if not id in self.get_keys(): return status
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(self): 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): def ota_generate(self, args, id=0):
command = ['ota_from_target_files'] command = ['ota_from_target_files']
@@ -71,15 +189,28 @@ class ProcessesManagement:
command.append(args['partial']) command.append(args['partial'])
command.append(args['target']) command.append(args['target'])
command.append(args['output']) command.append(args['output'])
# Start a subprocess and collect the output job_info = JobInfo(id,
stderr_pipes = pipes.Template() target=args['target'],
stdout_pipes = pipes.Template() incremental=args['incremental'] if args['isIncremental'] else '',
ferr = stderr_pipes.open(os.path.join( verbose=args['verbose'],
'output', 'stderr.'+str(id)), 'w') partial=args['partial'].split(
fout = stdout_pipes.open(os.path.join( ' ') if args['isPartial'] else [],
'output', 'stdout.'+str(id)), 'w') output=args['output'],
self.set(id, subprocess.Popen( status='Running',
command, stderr=ferr, stdout=fout)) 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( logging.info(
'Starting generating OTA package with id {}: \n {}' 'Starting generating OTA package with id {}: \n {}'
.format(id, command)) .format(id, command))

View File

@@ -0,0 +1,47 @@
<template>
<ul v-if="job">
<li>Start Time: {{ formDate(job.start_time) }}</li>
<li v-if="job.finish_time > 0">
Finish Time: {{ formDate(job.finish_time) }}
</li>
<li v-if="job.isIncremental">
Incremental source: {{ job.incremental_name }}
</li>
<li v-if="job.isIncremental && buildDetail">
Incremental source version: {{ job.incremental_build_version }}
</li>
<li>Target source: {{ job.target_name }}</li>
<li v-if="buildDetail">
Target source version: {{ job.target_build_version }}
</li>
<li v-if="job.isPartial">
Partial: {{ job.partial }}
</li>
</ul>
</template>
<script>
import FormDate from '../services/FormDate.js'
export default {
components: {
FormDate,
},
props: {
job: {
type: Object,
required: true,
default: null,
},
buildDetail: {
type: Boolean,
default: false,
},
},
methods: {
formDate(unixTime) {
return FormDate.formDate(unixTime)
},
},
}
</script>

View File

@@ -1,26 +1,39 @@
<template> <template>
<router-link <router-link :to="{ name: 'JobDetails', params: { id: job.id } }">
:to="{ name: 'JobDetails', params: {id: job.id} }"
>
<div class="job-display"> <div class="job-display">
<span>Status of Job.{{ job.id }}</span> <span>Status of Job.{{ job.id }}</span>
<h4>{{ job.status }}</h4> <h4>{{ job.status }}</h4>
<div v-show="active">
<JobConfiguration
:job="job"
:build-detail="false"
/>
</div>
</div> </div>
</router-link> </router-link>
</template> </template>
<script> <script>
import JobConfiguration from '../components/JobConfiguration.vue'
export default { export default {
components: {
JobConfiguration
},
props: { props: {
job: { job: {
type: Object, type: Object,
required: true required: true,
} },
active: {
type: Boolean,
default: false,
},
} }
} }
</script> </script>
<style scoped> <style>
.job-display { .job-display {
padding: 20px; padding: 20px;
width: 250px; width: 250px;

View File

@@ -0,0 +1,18 @@
export default{
formDate(unixTime) {
let formTime = new Date(unixTime * 1000)
let date =
formTime.getFullYear() +
'-' +
(formTime.getMonth() + 1) +
'-' +
formTime.getDate()
let time =
formTime.getHours() +
':' +
formTime.getMinutes() +
':' +
formTime.getSeconds()
return date + ' ' + time
}
}

View File

@@ -1,16 +1,26 @@
<template> <template>
<div v-if="job"> <div v-if="job">
<h3>Job. {{ job.id }} {{ job.status }}</h3> <h3>Job. {{ job.id }} {{ job.status }}</h3>
<JobConfiguration
:job="job"
:build-detail="true"
/>
<div> <div>
<h4>STDERR</h4> <h4>STDERR</h4>
<div class="stderr"> <div
ref="stderr"
class="stderr"
>
{{ job.stderr }} {{ job.stderr }}
<p ref="stderr_bottom" /> <p ref="stderrBottom" />
</div> </div>
<h4>STDOUT</h4> <h4>STDOUT</h4>
<div class="stdout"> <div
ref="stdout"
class="stdout"
>
{{ job.stdout }} {{ job.stdout }}
<p ref="stdout_bottom" /> <p ref="stdoutBottom" />
</div> </div>
</div> </div>
<br> <br>
@@ -22,9 +32,23 @@
</template> </template>
<script> <script>
import { ref } from 'vue'
import ApiService from '../services/ApiService.js' import ApiService from '../services/ApiService.js'
import JobConfiguration from '../components/JobConfiguration.vue'
export default { export default {
components: {
ApiService,
JobConfiguration,
},
props: ['id'], props: ['id'],
setup() {
const stderr = ref()
const stdout = ref()
const stderrBottom = ref()
const stdoutBottom = ref()
return { stderr, stdout, stderrBottom, stdoutBottom }
},
data() { data() {
return { return {
job: null, job: null,
@@ -32,7 +56,7 @@ export default {
}, },
computed: { computed: {
download() { download() {
return 'http://localhost:8000/download/' + this.job.path return 'http://localhost:8000/download/' + this.job.output
}, },
}, },
created() { created() {
@@ -44,15 +68,27 @@ export default {
try { try {
let response = await ApiService.getJobById(this.id) let response = await ApiService.getJobById(this.id)
this.job = response.data this.job = response.data
await this.$refs.stdout_bottom.scrollIntoView({ behavior: 'smooth' }) } catch (err) {
await this.$refs.stderr_bottom.scrollIntoView({ behavior: 'smooth' }) console.log(err)
}
try {
await this.$nextTick(() => {
this.stderr.scrollTo({
top: this.stderrBottom.offsetTop,
behavior: 'smooth',
})
this.stdout.scrollTo({
top: this.stdoutBottom.offsetTop,
behavior: 'smooth',
})
})
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }
if (this.job.status == 'Running') { if (this.job.status == 'Running') {
setTimeout(this.updateStatus, 1000) setTimeout(this.updateStatus, 1000)
} }
}, }
}, },
} }
</script> </script>
@@ -60,7 +96,7 @@ export default {
<style scoped> <style scoped>
.stderr, .stderr,
.stdout { .stdout {
overflow: scroll; overflow: scroll;
height: 200px; height: 200px;
} }
</style> </style>

View File

@@ -4,6 +4,9 @@
v-for="job in jobs" v-for="job in jobs"
:key="job.id" :key="job.id"
:job="job" :job="job"
:active="overStatus.get(job.id)"
@mouseover="mouseOver(job.id, true)"
@mouseout="mouseOver(job.id, false)"
/> />
<button @click="updateStatus"> <button @click="updateStatus">
Update Update
@@ -23,6 +26,7 @@ export default {
data() { data() {
return { return {
jobs: null, jobs: null,
overStatus: new Map()
} }
}, },
created (){ created (){
@@ -36,6 +40,9 @@ export default {
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
},
mouseOver(id, status) {
this.overStatus.set(id, status)
} }
} }
} }

View File

@@ -102,6 +102,7 @@ import FileSelect from '@/components/FileSelect.vue'
import ApiService from '../services/ApiService.js' import ApiService from '../services/ApiService.js'
import UploadFile from '@/components/UploadFile.vue' import UploadFile from '@/components/UploadFile.vue'
import PartialCheckbox from '@/components/PartialCheckbox.vue' import PartialCheckbox from '@/components/PartialCheckbox.vue'
import FormDate from '../services/FormDate.js'
import { uuid } from 'vue-uuid' import { uuid } from 'vue-uuid'
export default { export default {
@@ -111,6 +112,7 @@ export default {
UploadFile, UploadFile,
FileSelect, FileSelect,
PartialCheckbox, PartialCheckbox,
FormDate,
}, },
data() { data() {
return { return {
@@ -162,7 +164,8 @@ export default {
partial: '', partial: '',
isPartial: false, isPartial: false,
extra: '', extra: '',
} },
this.partitionInclude = new Map()
}, },
async sendForm(e) { async sendForm(e) {
try { try {
@@ -189,20 +192,7 @@ export default {
this.input.output += String(this.id) + '.zip' this.input.output += String(this.id) + '.zip'
}, },
formDate(unixTime) { formDate(unixTime) {
let formTime = new Date(unixTime * 1000) return FormDate.formDate(unixTime)
let date =
formTime.getFullYear() +
'-' +
(formTime.getMonth() + 1) +
'-' +
formTime.getDate()
let time =
formTime.getHours() +
':' +
formTime.getMinutes() +
':' +
formTime.getSeconds()
return date + ' ' + time
}, },
selectTarget(path) { selectTarget(path) {
this.input.target = path this.input.target = path

View File

@@ -68,6 +68,9 @@ class BuildInfo:
class TargetLib: class TargetLib:
"""
A class that manages the builds in database.
"""
def __init__(self, path='ota_database.db'): def __init__(self, path='ota_database.db'):
""" """
Create a build table if not existing Create a build table if not existing
@@ -142,7 +145,7 @@ class TargetLib:
FROM Builds""") FROM Builds""")
return list(map(self.sql_to_buildinfo, cursor.fetchall())) return list(map(self.sql_to_buildinfo, cursor.fetchall()))
def get_builds_by_path(self, path): def get_build_by_path(self, path):
""" """
Get a build in the database by its path Get a build in the database by its path
Return: Return:
@@ -153,6 +156,6 @@ class TargetLib:
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
WHERE Path==(?) FROM Builds WHERE Path==(?)
""", (path, )) """, (path, ))
return self.sql_to_buildinfo(cursor.fetchone()) return self.sql_to_buildinfo(cursor.fetchone())

View File

@@ -66,22 +66,19 @@ class RequestHandler(CORSSimpleHTTPHandler):
def do_GET(self): def do_GET(self):
if self.path.startswith('/check'): if self.path.startswith('/check'):
if self.path == '/check' or self.path == '/check/': if self.path == '/check' or self.path == '/check/':
status = jobs.get_status() statuses = jobs.get_status()
self._set_response(type='application/json') self._set_response(type='application/json')
self.wfile.write( self.wfile.write(
json.dumps(status).encode() json.dumps([status.to_dict_basic()
for status in statuses]).encode()
) )
else: else:
id = self.path[7:] id = self.path[7:]
status = jobs.get_status_by_ID(id=id, details=True) status = jobs.get_status_by_ID(id=id)
self._set_response(type='application/json') self._set_response(type='application/json')
self.wfile.write( self.wfile.write(
json.dumps(status).encode() json.dumps(status.to_dict_detail(target_lib)).encode()
) )
logging.info(
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers), status
)
return return
elif self.path.startswith('/file'): elif self.path.startswith('/file'):
if self.path == '/file' or self.path == '/file/': if self.path == '/file' or self.path == '/file/':