Merge "Add support for uploading and downloading." am: 316ce8acca am: 95f76c2367
Original change: https://android-review.googlesource.com/c/platform/development/+/1728650 Change-Id: I08248727a78ee8d74db320993247b917e1879772
This commit is contained in:
@@ -15,6 +15,13 @@ In this case we use `lunch 17` as an example (aosp-x86_64-cf), you can choose wh
|
|||||||
|
|
||||||
Then, in this directory, please use `npm build` to install the dependencies.
|
Then, in this directory, please use `npm build` to install the dependencies.
|
||||||
|
|
||||||
|
Create a `target` directory to store the target files and a `output` directory
|
||||||
|
to store the output files:
|
||||||
|
```
|
||||||
|
mkdir target
|
||||||
|
mkdir output
|
||||||
|
```
|
||||||
|
|
||||||
Finally, run the python http-server and vue.js server:
|
Finally, run the python http-server and vue.js server:
|
||||||
```
|
```
|
||||||
python3 web_server.py &
|
python3 web_server.py &
|
||||||
|
|||||||
86
tools/otagui/ota_interface.py
Normal file
86
tools/otagui/ota_interface.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pipes
|
||||||
|
from threading import Lock
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class ProcessesManagement:
|
||||||
|
def __init__(self):
|
||||||
|
self.__container = {}
|
||||||
|
self.__lock = Lock()
|
||||||
|
|
||||||
|
def set(self, name, value):
|
||||||
|
with self.__lock:
|
||||||
|
self.__container[name] = value
|
||||||
|
|
||||||
|
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(self):
|
||||||
|
return [self.get_status_by_ID(id=id) for id in self.get_keys()]
|
||||||
|
|
||||||
|
def get_list(self, dir):
|
||||||
|
files = os.listdir(dir)
|
||||||
|
return files
|
||||||
|
|
||||||
|
def ota_generate(self, args, id=0):
|
||||||
|
command = ['ota_from_target_files']
|
||||||
|
# Check essential configuration is properly set
|
||||||
|
if not os.path.isfile('target/' + args['target']):
|
||||||
|
raise FileNotFoundError
|
||||||
|
if not args['output']:
|
||||||
|
raise SyntaxError
|
||||||
|
if args['verbose']:
|
||||||
|
command.append('-v')
|
||||||
|
command.append('-k')
|
||||||
|
command.append(
|
||||||
|
'../../../build/make/target/product/security/testkey')
|
||||||
|
if args['incremental']:
|
||||||
|
if not os.path.isfile('target/' + args['incremental']):
|
||||||
|
raise FileNotFoundError
|
||||||
|
command.append('-i')
|
||||||
|
command.append('target/' + args['incremental'])
|
||||||
|
command.append('target/' + args['target'])
|
||||||
|
command.append(args['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))
|
||||||
|
logging.info(
|
||||||
|
'Starting generating OTA package with id {}: \n {}'
|
||||||
|
.format(id, command))
|
||||||
44
tools/otagui/package-lock.json
generated
44
tools/otagui/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"vue": "^3.0.0-0",
|
"vue": "^3.0.0-0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
|
"vue-uuid": "^2.0.2",
|
||||||
"vuex": "^4.0.0-0"
|
"vuex": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1479,6 +1480,11 @@
|
|||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
|
||||||
|
},
|
||||||
"node_modules/@types/webpack": {
|
"node_modules/@types/webpack": {
|
||||||
"version": "4.41.21",
|
"version": "4.41.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
|
||||||
@@ -13339,6 +13345,23 @@
|
|||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-uuid": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRf1CHg3uKi77bVRyAuW2u/T2PO9LxMr7cw9t9rNdpZTkNDyw1Fx6eJVL+8JOtM9VxxPkoZ/rwhXJ5l+X5AYzQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/uuid": "^8.0.0",
|
||||||
|
"uuid": "^8.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-uuid/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vuex": {
|
"node_modules/vuex": {
|
||||||
"version": "4.0.0-beta.4",
|
"version": "4.0.0-beta.4",
|
||||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",
|
||||||
@@ -15792,6 +15815,11 @@
|
|||||||
"source-map": "^0.6.1"
|
"source-map": "^0.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
|
||||||
|
},
|
||||||
"@types/webpack": {
|
"@types/webpack": {
|
||||||
"version": "4.41.21",
|
"version": "4.41.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.21.tgz",
|
||||||
@@ -25710,6 +25738,22 @@
|
|||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"vue-uuid": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-PRf1CHg3uKi77bVRyAuW2u/T2PO9LxMr7cw9t9rNdpZTkNDyw1Fx6eJVL+8JOtM9VxxPkoZ/rwhXJ5l+X5AYzQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/uuid": "^8.0.0",
|
||||||
|
"uuid": "^8.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vuex": {
|
"vuex": {
|
||||||
"version": "4.0.0-beta.4",
|
"version": "4.0.0-beta.4",
|
||||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.0-beta.4.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"vue": "^3.0.0-0",
|
"vue": "^3.0.0-0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
|
"vue-uuid": "^2.0.2",
|
||||||
"vuex": "^4.0.0-0"
|
"vuex": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<label class="file-select">
|
<label class="file-select">
|
||||||
<div class="select-button">
|
<div class="select-button">
|
||||||
<span v-if="value">Selected File: {{ value.name }}</span>
|
<span v-if="label">{{ label }}</span>
|
||||||
<span v-else>Select File</span>
|
<span v-else>Select File</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -14,12 +14,14 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: File
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleFileChange(e) {
|
handleFileChange(e) {
|
||||||
this.$emit('input', e.target.files[0])
|
this.$emit('file-select', e.target.files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<label> {{ label }} </label>
|
<label> {{ label }} </label>
|
||||||
|
<br>
|
||||||
<input
|
<input
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:placeholder="label"
|
:placeholder="label"
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="job-display">
|
<router-link
|
||||||
<span>Status of Job.{{ job.id }}</span>
|
:to="{ name: 'JobDetails', params: {id: job.id} }"
|
||||||
<h4>{{ job.status }}</h4>
|
>
|
||||||
</div>
|
<div class="job-display">
|
||||||
|
<span>Status of Job.{{ job.id }}</span>
|
||||||
|
<h4>{{ job.status }}</h4>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
53
tools/otagui/src/components/UploadFile.vue
Normal file
53
tools/otagui/src/components/UploadFile.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BaseFile
|
||||||
|
label="Upload a target file"
|
||||||
|
@file-select="UploadHandler"
|
||||||
|
/>
|
||||||
|
<br>
|
||||||
|
<progress
|
||||||
|
v-if="uploadPercentage > 0"
|
||||||
|
max="100"
|
||||||
|
:value.prop="uploadPercentage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseFile from '@/components/BaseFile.vue'
|
||||||
|
import ApiService from '../services/ApiService.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BaseFile,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
files: '',
|
||||||
|
uploadPercentage: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async UploadHandler(files) {
|
||||||
|
this.file = files[0]
|
||||||
|
console.log(this.file.name)
|
||||||
|
try {
|
||||||
|
let response = await ApiService.uploadTarget(this.file, (event) => {
|
||||||
|
this.uploadPercentage = Math.round((100 * event.loaded) / event.total)
|
||||||
|
})
|
||||||
|
console.log(response)
|
||||||
|
this.$emit('file-uploaded')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: 'http://localhost:8000',
|
baseURL: 'http://localhost:8000',
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -13,11 +13,25 @@ export default {
|
|||||||
getJobs() {
|
getJobs() {
|
||||||
return apiClient.get("/check")
|
return apiClient.get("/check")
|
||||||
},
|
},
|
||||||
|
getJobById(id) {
|
||||||
|
return apiClient.get("/check/" + id)
|
||||||
|
},
|
||||||
|
getFileList(path) {
|
||||||
|
return apiClient.get("/file" + path)
|
||||||
|
},
|
||||||
|
uploadTarget(file, onUploadProgress) {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return apiClient.post("/file/" + file.name,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
onUploadProgress
|
||||||
|
})
|
||||||
|
},
|
||||||
async postInput(input, id) {
|
async postInput(input, id) {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(
|
||||||
'/run/' + id, input)
|
'/run/' + id, input)
|
||||||
console.log('Response:', response)
|
|
||||||
return response
|
return response
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('err:', err)
|
console.log('err:', err)
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="job">
|
<div v-if="job">
|
||||||
<h3> Job. {{ job.id }} {{ job.status }}</h3>
|
<h3>Job. {{ job.id }} {{ job.status }}</h3>
|
||||||
<div>
|
<div>
|
||||||
<h4> STDERR </h4>
|
<h4>STDERR</h4>
|
||||||
<div class="stderr">
|
<div class="stderr">
|
||||||
{{ job.stderr }}
|
{{ job.stderr }}
|
||||||
|
<p ref="stderr_bottom" />
|
||||||
</div>
|
</div>
|
||||||
<h4> STDOUT </h4>
|
<h4>STDOUT</h4>
|
||||||
<div class="stdout">
|
<div class="stdout">
|
||||||
{{ job.stdout }}
|
{{ job.stdout }}
|
||||||
|
<p ref="stdout_bottom" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<a
|
<a
|
||||||
v-if="job.status=='Finished'"
|
v-if="job.status == 'Finished'"
|
||||||
:href="download"
|
:href="download"
|
||||||
>
|
> Download </a>
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -32,33 +32,35 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
download() {
|
download() {
|
||||||
return "http://localhost:8000/download/" + this.job.path
|
return 'http://localhost:8000/download/' + this.job.path
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.updateStatus()
|
this.updateStatus()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async updateStatus() {
|
async updateStatus() {
|
||||||
// fetch job (by id) and set local job data
|
// fetch job (by id) and set local job data
|
||||||
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' })
|
||||||
|
await this.$refs.stderr_bottom.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
}
|
}
|
||||||
catch (err) {
|
if (this.job.status == 'Running') {
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
if (this.job.status=='Running') {
|
|
||||||
setTimeout(this.updateStatus, 1000)
|
setTimeout(this.updateStatus, 1000)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stderr, .stdout {
|
.stderr,
|
||||||
overflow: scroll;
|
.stdout {
|
||||||
height: 200px;
|
overflow: scroll;
|
||||||
|
height: 200px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,57 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<form @submit.prevent="sendForm">
|
<form @submit.prevent="sendForm">
|
||||||
<BaseInput
|
<UploadFile @file-uploaded="fetchTargetList" />
|
||||||
|
<br>
|
||||||
|
<BaseSelect
|
||||||
|
v-if="input.incrementalStatus"
|
||||||
v-model="input.incremental"
|
v-model="input.incremental"
|
||||||
:disabled="!input.incrementalStatus"
|
label="Select the source file"
|
||||||
:label="'Source Package Path'"
|
:options="targetList"
|
||||||
type="text"
|
|
||||||
/>
|
/>
|
||||||
|
<BaseSelect
|
||||||
<BaseInput
|
|
||||||
v-model="input.target"
|
v-model="input.target"
|
||||||
label="Target File path"
|
label="Select the target file"
|
||||||
type="text"
|
:options="targetList"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
<BaseCheckbox
|
type="button"
|
||||||
v-model="input.verbose"
|
@click="fetchTargetList"
|
||||||
:label="'Verbose'"
|
>
|
||||||
/>
|
Update File List
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
<BaseCheckbox
|
<BaseCheckbox
|
||||||
v-model="input.incrementalStatus"
|
v-model="input.verbose"
|
||||||
:label="'Incremental'"
|
:label="'Verbose'"
|
||||||
/>
|
/>
|
||||||
|
 
|
||||||
|
<BaseCheckbox
|
||||||
|
v-model="input.incrementalStatus"
|
||||||
|
:label="'Incremental'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
<BaseInput
|
<BaseInput
|
||||||
v-model="input.output"
|
v-model="input.extra"
|
||||||
label="Output File path"
|
:label="'Extra Configurations'"
|
||||||
type="text"
|
|
||||||
/>
|
/>
|
||||||
|
<br>
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<pre> {{ input }} </pre>
|
|
||||||
|
|
||||||
<h3> Response from the server </h3>
|
|
||||||
<div> {{ response_message }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseInput from '@/components/BaseInput.vue'
|
import BaseInput from '@/components/BaseInput.vue'
|
||||||
import BaseCheckbox from '@/components/BaseCheckbox.vue'
|
import BaseCheckbox from '@/components/BaseCheckbox.vue'
|
||||||
|
import BaseSelect from '@/components/BaseSelect.vue'
|
||||||
import ApiService from '../services/ApiService.js'
|
import ApiService from '../services/ApiService.js'
|
||||||
|
import UploadFile from '@/components/UploadFile.vue'
|
||||||
|
import { uuid } from 'vue-uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BaseInput,
|
BaseInput,
|
||||||
BaseCheckbox
|
BaseCheckbox,
|
||||||
|
UploadFile,
|
||||||
|
BaseSelect,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -59,31 +65,61 @@ export default {
|
|||||||
input: {
|
input: {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
target: '',
|
target: '',
|
||||||
output: '',
|
output: 'output/',
|
||||||
incremental: '',
|
incremental: '',
|
||||||
incrementalStatus: false
|
incrementalStatus: false,
|
||||||
|
extra: '',
|
||||||
},
|
},
|
||||||
inputs: [],
|
inputs: [],
|
||||||
response_message : ''
|
response_message: '',
|
||||||
|
targetList: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods:{
|
computed: {
|
||||||
|
updateOutput() {
|
||||||
|
return 'output/' + String(this.id) + '.zip'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchTargetList()
|
||||||
|
this.updateUUID()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
sendForm(e) {
|
sendForm(e) {
|
||||||
|
// console.log(this.input)
|
||||||
ApiService.postInput(this.input, this.id)
|
ApiService.postInput(this.input, this.id)
|
||||||
.then(Response => {
|
.then((Response) => {
|
||||||
this.response_message = Response.data
|
this.response_message = Response.data
|
||||||
|
alert(this.response_message)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
this.response_message = 'Error! ' + err
|
this.response_message = 'Error! ' + err
|
||||||
})
|
})
|
||||||
this.input = {
|
this.input = {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
target: '',
|
target: '',
|
||||||
output: '',
|
output: 'output/',
|
||||||
incremental: '',
|
incremental: '',
|
||||||
incrementalStatus: false
|
incrementalStatus: false,
|
||||||
|
extra: '',
|
||||||
}
|
}
|
||||||
}
|
this.updateUUID()
|
||||||
|
},
|
||||||
|
async fetchTargetList() {
|
||||||
|
try {
|
||||||
|
let response = await ApiService.getFileList('/target')
|
||||||
|
this.targetList = response.data
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Fetch Error', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateUUID() {
|
||||||
|
this.id = uuid.v1()
|
||||||
|
this.input.output += String(this.id) + '.zip'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
A simple local HTTP server for Android OTA package generation.
|
A local HTTP server for Android OTA package generation.
|
||||||
Based on OTA_from_target_files.
|
Based on OTA_from_target_files.py
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
python ./web_server.py [<port>]
|
python ./web_server.py [<port>]
|
||||||
|
|
||||||
API::
|
API::
|
||||||
GET /check : check the status of all jobs
|
GET /check : check the status of all jobs
|
||||||
[TODO] GET /check/id : check the status of the job with <id>
|
GET /check/<id> : check the status of the job with <id>
|
||||||
POST /run/id : submit a job with <id>,
|
GET /file : fetch the target file list
|
||||||
|
GET /download/<id> : download the ota package with <id>
|
||||||
|
POST /run/<id> : submit a job with <id>,
|
||||||
arguments set in a json uploaded together
|
arguments set in a json uploaded together
|
||||||
[TODO] POST /cancel/id : cancel a job with <id>
|
POST /file/<filename> : upload a target file
|
||||||
|
[TODO] POST /cancel/<id> : cancel a job with <id>
|
||||||
|
|
||||||
|
Other GET request will be redirected to the static request under 'dist' directory
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, SimpleHTTPRequestHandler, HTTPServer
|
||||||
from socketserver import ThreadingMixIn
|
from socketserver import ThreadingMixIn
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
from ota_interface import ProcessesManagement
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import pipes
|
import pipes
|
||||||
@@ -25,123 +32,114 @@ import os
|
|||||||
LOCAL_ADDRESS = '0.0.0.0'
|
LOCAL_ADDRESS = '0.0.0.0'
|
||||||
|
|
||||||
|
|
||||||
class ThreadSafeContainer:
|
class CORSSimpleHTTPHandler(SimpleHTTPRequestHandler):
|
||||||
def __init__(self):
|
def end_headers(self):
|
||||||
self.__container = {}
|
|
||||||
self.__lock = Lock()
|
|
||||||
|
|
||||||
def set(self, name, value):
|
|
||||||
with self.__lock:
|
|
||||||
self.__container[name] = value
|
|
||||||
|
|
||||||
def get(self, name):
|
|
||||||
with self.__lock:
|
|
||||||
return self.__container[name]
|
|
||||||
|
|
||||||
def get_keys(self):
|
|
||||||
with self.__lock:
|
|
||||||
return self.__container.keys()
|
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler(BaseHTTPRequestHandler):
|
|
||||||
def get_status(self):
|
|
||||||
statusList = []
|
|
||||||
for id in PROCESSES.get_keys():
|
|
||||||
status = {}
|
|
||||||
status['id'] = id
|
|
||||||
if PROCESSES.get(id).poll() == None:
|
|
||||||
status['status'] = 'Running'
|
|
||||||
elif PROCESSES.get(id).poll() == 0:
|
|
||||||
status['status'] = 'Finished'
|
|
||||||
else:
|
|
||||||
status['status'] = 'Error'
|
|
||||||
statusList.append(json.dumps(status))
|
|
||||||
return '['+','.join(statusList)+']'
|
|
||||||
|
|
||||||
def ota_generate(self, args, id=0):
|
|
||||||
command = ['ota_from_target_files']
|
|
||||||
# Check essential configuration is properly set
|
|
||||||
if not os.path.isfile(args['target']):
|
|
||||||
raise FileNotFoundError
|
|
||||||
if not args['output']:
|
|
||||||
raise SyntaxError
|
|
||||||
if args['verbose']:
|
|
||||||
command.append('-v')
|
|
||||||
command.append('-k')
|
|
||||||
command.append(
|
|
||||||
'../../../build/make/target/product/security/testkey')
|
|
||||||
if args['incremental']:
|
|
||||||
command.append('-i')
|
|
||||||
command.append(args['incremental'])
|
|
||||||
command.append(args['target'])
|
|
||||||
command.append(args['output'])
|
|
||||||
stderr_pipes = pipes.Template()
|
|
||||||
stdout_pipes = pipes.Template()
|
|
||||||
ferr = stderr_pipes.open('stderr', 'w')
|
|
||||||
fout = stdout_pipes.open('stdout', 'w')
|
|
||||||
PROCESSES.set(id, subprocess.Popen(
|
|
||||||
command, stderr=ferr, stdout=fout))
|
|
||||||
logging.info(
|
|
||||||
'Starting generating OTA package with id {}: \n {}'
|
|
||||||
.format(id, command))
|
|
||||||
|
|
||||||
def _set_response(self, type='text/html'):
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-type', type)
|
|
||||||
try:
|
try:
|
||||||
origin_address, _ = cgi.parse_header(self.headers['Origin'])
|
origin_address, _ = cgi.parse_header(self.headers['Origin'])
|
||||||
self.send_header('Access-Control-Allow-Credentials', 'true')
|
self.send_header('Access-Control-Allow-Credentials', 'true')
|
||||||
self.send_header('Access-Control-Allow-Origin', origin_address)
|
self.send_header('Access-Control-Allow-Origin', origin_address)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(CORSSimpleHTTPHandler):
|
||||||
|
def _set_response(self, code=200, type='text/html'):
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header('Content-type', type)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
|
||||||
self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
|
self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
|
||||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if str(self.path) == '/check':
|
if self.path.startswith('/check'):
|
||||||
status = self.get_status()
|
if self.path == '/check' or self.path == '/check/':
|
||||||
self._set_response('application/json')
|
status = jobs.get_status()
|
||||||
self.wfile.write(
|
self._set_response(type='application/json')
|
||||||
status.encode()
|
self.wfile.write(
|
||||||
|
json.dumps(status).encode()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
id = self.path[7:]
|
||||||
|
status = jobs.get_status_by_ID(id=id, details=True)
|
||||||
|
self._set_response(type='application/json')
|
||||||
|
self.wfile.write(
|
||||||
|
json.dumps(status).encode()
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
||||||
|
str(self.path), str(self.headers), status
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
return
|
return
|
||||||
logging.info(
|
elif self.path.startswith('/file'):
|
||||||
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
file_list = jobs.get_list(self.path[6:])
|
||||||
str(self.path), str(self.headers), status
|
self._set_response(type='application/json')
|
||||||
)
|
self.wfile.write(
|
||||||
|
json.dumps(file_list).encode()
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
||||||
|
str(self.path), str(self.headers), file_list
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif self.path.startswith('/download'):
|
||||||
|
self.path = self.path[10:]
|
||||||
|
return CORSSimpleHTTPHandler.do_GET(self)
|
||||||
|
else:
|
||||||
|
self.path = '/dist' + self.path
|
||||||
|
return CORSSimpleHTTPHandler.do_GET(self)
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
content_type, _ = cgi.parse_header(self.headers['content-type'])
|
if self.path.startswith('/run'):
|
||||||
if content_type != 'application/json':
|
content_type, _ = cgi.parse_header(self.headers['content-type'])
|
||||||
self.send_response(400)
|
if content_type != 'application/json':
|
||||||
self.end_headers()
|
self.send_response(400)
|
||||||
return
|
self.end_headers()
|
||||||
content_length = int(self.headers['Content-Length'])
|
return
|
||||||
post_data = json.loads(self.rfile.read(content_length))
|
content_length = int(self.headers['Content-Length'])
|
||||||
if str(self.path)[:4] == '/run':
|
post_data = json.loads(self.rfile.read(content_length))
|
||||||
try:
|
try:
|
||||||
self.ota_generate(post_data, id=str(self.path[5:]))
|
jobs.ota_generate(post_data, id=str(self.path[5:]))
|
||||||
self._set_response()
|
self._set_response(code=201)
|
||||||
self.wfile.write(
|
self.wfile.write(
|
||||||
"ota generator start running".encode('utf-8'))
|
"ota generator start running".encode('utf-8'))
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
self.send_error(400)
|
self.send_error(400)
|
||||||
|
logging.info(
|
||||||
|
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
||||||
|
str(self.path), str(self.headers),
|
||||||
|
json.dumps(post_data)
|
||||||
|
)
|
||||||
|
elif self.path.startswith('/file'):
|
||||||
|
file_name = os.path.join('target', self.path[6:])
|
||||||
|
file_length = int(self.headers['Content-Length'])
|
||||||
|
with open(file_name, 'wb') as output_file:
|
||||||
|
# Unwrap the uploaded file first (due to the usage of FormData)
|
||||||
|
# The wrapper has a boundary line at the top and bottom
|
||||||
|
# and some file information in the beginning
|
||||||
|
# There are a file content line, a file name line, and an empty line
|
||||||
|
# The boundary line in the bottom is 4 bytes longer than the top one
|
||||||
|
# Please refer to the following links for more details:
|
||||||
|
# https://stackoverflow.com/questions/8659808/how-does-http-file-upload-work
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc1867
|
||||||
|
upper_boundary = self.rfile.readline()
|
||||||
|
file_length -= len(upper_boundary) * 2 + 4
|
||||||
|
file_length -= len(self.rfile.readline())
|
||||||
|
file_length -= len(self.rfile.readline())
|
||||||
|
file_length -= len(self.rfile.readline())
|
||||||
|
output_file.write(self.rfile.read(file_length))
|
||||||
|
self._set_response(code=201)
|
||||||
|
self.wfile.write(
|
||||||
|
"File received, saved into {}".format(
|
||||||
|
file_name).encode('utf-8')
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.send_error(400)
|
self.send_error(400)
|
||||||
logging.info(
|
|
||||||
"POST request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
|
|
||||||
str(self.path), str(self.headers),
|
|
||||||
json.dumps(post_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
@@ -166,7 +164,7 @@ def run_server(SeverClass=ThreadedHTTPServer, HandlerClass=RequestHandler, port=
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from sys import argv
|
from sys import argv
|
||||||
print(argv)
|
print(argv)
|
||||||
PROCESSES = ThreadSafeContainer()
|
jobs = ProcessesManagement()
|
||||||
if len(argv) == 2:
|
if len(argv) == 2:
|
||||||
run_server(port=int(argv[1]))
|
run_server(port=int(argv[1]))
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user