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

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

Change-Id: Ibb6b53298a916fd8f14b6607786628c163f5c8c6
This commit is contained in:
Treehugger Robot
2021-06-21 23:15:54 +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 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))

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>
<router-link
:to="{ name: 'JobDetails', params: {id: job.id} }"
>
<router-link :to="{ name: 'JobDetails', params: { id: job.id } }">
<div class="job-display">
<span>Status of Job.{{ job.id }}</span>
<h4>{{ job.status }}</h4>
<div v-show="active">
<JobConfiguration
:job="job"
:build-detail="false"
/>
</div>
</div>
</router-link>
</template>
<script>
import JobConfiguration from '../components/JobConfiguration.vue'
export default {
components: {
JobConfiguration
},
props: {
job: {
type: Object,
required: true
}
required: true,
},
active: {
type: Boolean,
default: false,
},
}
}
</script>
<style scoped>
<style>
.job-display {
padding: 20px;
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>
<div v-if="job">
<h3>Job. {{ job.id }} {{ job.status }}</h3>
<JobConfiguration
:job="job"
:build-detail="true"
/>
<div>
<h4>STDERR</h4>
<div class="stderr">
<div
ref="stderr"
class="stderr"
>
{{ job.stderr }}
<p ref="stderr_bottom" />
<p ref="stderrBottom" />
</div>
<h4>STDOUT</h4>
<div class="stdout">
<div
ref="stdout"
class="stdout"
>
{{ job.stdout }}
<p ref="stdout_bottom" />
<p ref="stdoutBottom" />
</div>
</div>
<br>
@@ -22,9 +32,23 @@
</template>
<script>
import { ref } from 'vue'
import ApiService from '../services/ApiService.js'
import JobConfiguration from '../components/JobConfiguration.vue'
export default {
components: {
ApiService,
JobConfiguration,
},
props: ['id'],
setup() {
const stderr = ref()
const stdout = ref()
const stderrBottom = ref()
const stdoutBottom = ref()
return { stderr, stdout, stderrBottom, stdoutBottom }
},
data() {
return {
job: null,
@@ -32,7 +56,7 @@ export default {
},
computed: {
download() {
return 'http://localhost:8000/download/' + this.job.path
return 'http://localhost:8000/download/' + this.job.output
},
},
created() {
@@ -44,15 +68,27 @@ export default {
try {
let response = await ApiService.getJobById(this.id)
this.job = response.data
await this.$refs.stdout_bottom.scrollIntoView({ behavior: 'smooth' })
await this.$refs.stderr_bottom.scrollIntoView({ behavior: 'smooth' })
} catch (err) {
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) {
console.log(err)
}
if (this.job.status == 'Running') {
setTimeout(this.updateStatus, 1000)
}
},
}
},
}
</script>
@@ -60,7 +96,7 @@ export default {
<style scoped>
.stderr,
.stdout {
overflow: scroll;
height: 200px;
overflow: scroll;
height: 200px;
}
</style>

View File

@@ -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)"
/>
<button @click="updateStatus">
Update
@@ -23,6 +26,7 @@ export default {
data() {
return {
jobs: null,
overStatus: new Map()
}
},
created (){
@@ -36,6 +40,9 @@ export default {
} catch (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 UploadFile from '@/components/UploadFile.vue'
import PartialCheckbox from '@/components/PartialCheckbox.vue'
import FormDate from '../services/FormDate.js'
import { uuid } from 'vue-uuid'
export default {
@@ -111,6 +112,7 @@ export default {
UploadFile,
FileSelect,
PartialCheckbox,
FormDate,
},
data() {
return {
@@ -162,7 +164,8 @@ export default {
partial: '',
isPartial: false,
extra: '',
}
},
this.partitionInclude = new Map()
},
async sendForm(e) {
try {
@@ -189,20 +192,7 @@ export default {
this.input.output += String(this.id) + '.zip'
},
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
return FormDate.formDate(unixTime)
},
selectTarget(path) {
this.input.target = path

View File

@@ -68,6 +68,9 @@ class BuildInfo:
class TargetLib:
"""
A class that manages the builds in database.
"""
def __init__(self, path='ota_database.db'):
"""
Create a build table if not existing
@@ -142,7 +145,7 @@ class TargetLib:
FROM Builds""")
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
Return:
@@ -153,6 +156,6 @@ class TargetLib:
cursor = connect.cursor()
cursor.execute("""
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
WHERE Path==(?)
FROM Builds WHERE Path==(?)
""", (path, ))
return self.sql_to_buildinfo(cursor.fetchone())

View File

@@ -66,22 +66,19 @@ class RequestHandler(CORSSimpleHTTPHandler):
def do_GET(self):
if self.path.startswith('/check'):
if self.path == '/check' or self.path == '/check/':
status = jobs.get_status()
statuses = jobs.get_status()
self._set_response(type='application/json')
self.wfile.write(
json.dumps(status).encode()
json.dumps([status.to_dict_basic()
for status in statuses]).encode()
)
else:
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.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
elif self.path.startswith('/file'):
if self.path == '/file' or self.path == '/file/':