Merge changes Ic5fdefe2,I32cbd027,I464d846c,Ie9ef8e2b,Ib7b02d5c

* changes:
  API Service should return json data directly
  Rename path to db_path
  Ignore vscode files in docker
  Return error in json
  Delegate choice of output file path to 1 place in code
This commit is contained in:
Kelvin Zhang
2021-09-01 14:21:26 +00:00
committed by Gerrit Code Review
17 changed files with 215 additions and 157 deletions

View File

@@ -1,2 +1,7 @@
node_modules/
Dockerfile
Dockerfile
.vscode
dist
output
target
*.db

View File

@@ -28,3 +28,6 @@ pnpm-debug.log*
*.sln
*.sw?
*.db
packaged
**/.DS_Store

View File

@@ -3,7 +3,12 @@ FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
COPY src ./src
COPY public ./public
COPY *.js .
COPY .env* .
COPY .eslint* .
RUN npm run build
# production stage
@@ -14,7 +19,7 @@ WORKDIR /app
VOLUME [ "/app/target", "/app/output"]
COPY otatools.zip .
COPY --from=build-stage /app/dist ./dist
COPY --from=build-stage /app/*.py .
COPY *.py .
EXPOSE 8000
CMD ["python3.9", "web_server.py"]

8
tools/otagui/build_zip.bash Executable file
View 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}

View File

@@ -1,6 +1,5 @@
import subprocess
import os
import json
import pipes
import threading
from dataclasses import dataclass, asdict, field
@@ -31,16 +30,6 @@ class JobInfo:
isIncremental: bool = False
def __post_init__(self):
"""
If the output, stdout, stderr paths are not set, automatically use
the job id as the file name.
"""
if not self.output:
self.output = os.path.join('output', self.id + '.zip')
if not self.stdout:
self.stdout = os.path.join('output/stdout.'+self.id)
if not self.stderr:
self.stderr = os.path.join('output/stderr.'+self.id)
def enforce_bool(t): return t if isinstance(t, bool) else bool(t)
self.verbose, self.downgrade = map(
@@ -108,16 +97,43 @@ class JobInfo:
return detail_info
class DependencyError(Exception):
pass
class ProcessesManagement:
"""
A class manage the ota generate process
"""
def __init__(self, path='output/ota_database.db'):
@staticmethod
def check_external_dependencies():
try:
java_version = subprocess.check_output(["java", "--version"])
print("Java version:", java_version.decode())
except Exception as e:
raise DependencyError(
"java not found in PATH. Attempt to generate OTA might fail. " + str(e))
try:
zip_version = subprocess.check_output(["zip", "-v"])
print("Zip version:", zip_version.decode())
except Exception as e:
raise DependencyError(
"zip command not found in PATH. Attempt to generate OTA might fail. " + str(e))
def __init__(self, *, working_dir='output', db_path=None, otatools_dir=None):
"""
create a table if not exist
"""
self.path = path
ProcessesManagement.check_external_dependencies()
self.working_dir = working_dir
self.logs_dir = os.path.join(working_dir, 'logs')
self.otatools_dir = otatools_dir
os.makedirs(self.working_dir, exist_ok=True)
os.makedirs(self.logs_dir, exist_ok=True)
if not db_path:
db_path = os.path.join(self.working_dir, "ota_database.db")
self.path = db_path
with sqlite3.connect(self.path) as connect:
cursor = connect.cursor()
cursor.execute("""
@@ -145,8 +161,8 @@ class ProcessesManagement:
job_info: JobInfo
"""
with sqlite3.connect(self.path) as connect:
cursor = connect.cursor()
cursor.execute("""
cursor = connect.cursor()
cursor.execute("""
INSERT INTO Jobs (ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, Finishtime)
VALUES (:id, :target, :incremental, :verbose, :partial, :output, :status, :downgrade, :extra, :stdout, :stderr, :start_time, :finish_time)
""", job_info.to_sql_form_dict())
@@ -165,7 +181,7 @@ class ProcessesManagement:
cursor.execute("""
SELECT ID, TargetPath, IncrementalPath, Verbose, Partial, OutputPath, Status, Downgrade, OtherFlags, STDOUT, STDERR, StartTime, FinishTime
FROM Jobs WHERE ID=(?)
""", (id,))
""", (str(id),))
row = cursor.fetchone()
status = JobInfo(*row)
return status
@@ -202,32 +218,41 @@ class ProcessesManagement:
""",
(status, finish_time, id))
def ota_run(self, command, id):
def ota_run(self, command, id, stdout_path, stderr_path):
"""
Initiate a subprocess to run the ota generation. Wait until it finished and update
the record in the database.
"""
stderr_pipes = pipes.Template()
stdout_pipes = pipes.Template()
ferr = stderr_pipes.open(stdout_path, 'w')
fout = stdout_pipes.open(stderr_path, 'w')
env = {}
if self.otatools_dir:
env['PATH'] = os.path.join(
self.otatools_dir, "bin") + ":" + os.environ["PATH"]
# TODO(lishutong): Enable user to use self-defined stderr/stdout path
ferr = stderr_pipes.open(os.path.join(
'output', 'stderr.'+str(id)), 'w')
fout = stdout_pipes.open(os.path.join(
'output', 'stdout.'+str(id)), 'w')
try:
proc = subprocess.Popen(
command, stderr=ferr, stdout=fout, shell=False)
command, stderr=ferr, stdout=fout, shell=False, env=env, cwd=self.otatools_dir)
except FileNotFoundError as e:
logging.error('ota_from_target_files is not set properly %s', e)
self.update_status(id, 'Error', int(time.time()))
return
exit_code = proc.wait()
if exit_code == 0:
self.update_status(id, 'Finished', int(time.time()))
else:
raise
except Exception as e:
logging.error('Failed to execute ota_from_target_files %s', e)
self.update_status(id, 'Error', int(time.time()))
raise
def ota_generate(self, args, id=0):
def wait_result():
exit_code = proc.wait()
if exit_code == 0:
self.update_status(id, 'Finished', int(time.time()))
else:
self.update_status(id, 'Error', int(time.time()))
threading.Thread(target=wait_result).start()
def ota_generate(self, args, id):
"""
Read in the arguments from the frontend and start running the OTA
generation process, then update the records in database.
@@ -244,40 +269,41 @@ class ProcessesManagement:
if not os.path.isfile(args['target']):
raise FileNotFoundError
if not 'output' in args:
args['output'] = os.path.join('output', str(id) + '.zip')
args['output'] = os.path.join(self.working_dir, str(id) + '.zip')
if args['verbose']:
command.append('-v')
if args['extra_keys']:
args['extra'] = \
'--' + ' --'.join(args['extra_keys']) + ' ' + args['extra']
args['extra'] = '--' + \
' --'.join(args['extra_keys']) + ' ' + args['extra']
if args['extra']:
command += args['extra'].strip().split(' ')
if args['isIncremental']:
if not os.path.isfile(args['incremental']):
raise FileNotFoundError
command.append('-i')
command.append(args['incremental'])
command.append(os.path.realpath(args['incremental']))
if args['isPartial']:
command.append('--partial')
command.append(' '.join(args['partial']))
command.append(args['target'])
command.append(args['output'])
command.append(os.path.realpath(args['target']))
command.append(os.path.realpath(args['output']))
stdout = os.path.join(self.logs_dir, 'stdout.' + str(id))
stderr = os.path.join(self.logs_dir, 'stderr.' + str(id))
job_info = JobInfo(id,
target=args['target'],
incremental=args['incremental'] if args['isIncremental'] else '',
verbose=args['verbose'],
partial=args['partial'] if args['isPartial'] else [],
partial=args['partial'] if args['isPartial'] else [
],
output=args['output'],
status='Running',
extra=args['extra'],
start_time=int(time.time())
start_time=int(time.time()),
stdout=stdout,
stderr=stderr
)
try:
thread = threading.Thread(target=self.ota_run, args=(command, id))
self.insert_database(job_info)
thread.start()
except AssertionError:
raise SyntaxError
self.ota_run(command, id, job_info.stdout, job_info.stderr)
self.insert_database(job_info)
logging.info(
'Starting generating OTA package with id {}: \n {}'
.format(id, command))

View File

@@ -74,8 +74,9 @@ export default {
*/
async sendForm() {
try {
let response_messages = await this.$store.state.otaConfig.sendForms(
this.targetBuilds, this.incrementalSources)
let response_data = await this.$store.state.otaConfig.sendForms(
this.targetBuilds, this.incrementalSources);
let response_messages = response_data.map(d => d.msg);
alert(response_messages.join('\n'))
this.$store.state.otaConfig.reset()
this.$store.commit('SET_TARGETS', [])

View File

@@ -3,7 +3,7 @@
<h3>Build Library</h3>
<UploadFile @file-uploaded="fetchTargetList" />
<BuildTable
v-if="targetDetails.length>0"
v-if="targetDetails && targetDetails.length>0"
:builds="targetDetails"
/>
<li
@@ -42,7 +42,7 @@
<script>
import UploadFile from '@/components/UploadFile.vue'
import BuildTable from '@/components/BuildTable.vue'
import ApiService from '@/services/ApiService.js'
import ApiService from '../services/ApiService.js'
import FormDate from '@/services/FormDate.js'
export default {
@@ -72,9 +72,8 @@ export default {
*/
async fetchTargetList() {
try {
let response = await ApiService.getFileList('')
this.targetDetails = response.data
this.$emit('update:targetDetails', response.data)
this.targetDetails = await ApiService.getBuildList()
this.$emit('update:targetDetails', this.targetDetails)
} catch (err) {
alert(
"Cannot fetch Android Builds list from the backend, for the following reasons:"
@@ -88,9 +87,8 @@ export default {
*/
async updateBuildLib() {
try {
let response = await ApiService.getFileList('/target')
this.targetDetails = response.data
this.$emit('update:targetDetails', response.data)
this.targetDetails = await ApiService.reconstructBuildList();
this.$emit('update:targetDetails', this.targetDetails);
} catch (err) {
alert(
"Cannot fetch Android Builds list from the backend, for the following reasons: "

View File

@@ -74,12 +74,13 @@ export default {
if (this.targetBuilds.length<2) {
alert(
'At least two OTA packeges has to be given!'
)
);
return
}
try {
let response_messages = await this.$store.state.otaConfig
let response_data = await this.$store.state.otaConfig
.sendChainForms(this.targetBuilds)
let response_messages = response_data.map(d => d.msg);
alert(response_messages.join('\n'))
this.$store.state.otaConfig.reset()
this.$store.commit('SET_TARGETS', [])

View File

@@ -74,9 +74,9 @@ export default {
*/
async sendForm() {
try {
let response_message = await this.$store.state.otaConfig.sendForm(
let data = await this.$store.state.otaConfig.sendForm(
this.targetBuild, this.incrementalSource)
alert(response_message)
alert(data.msg);
this.$store.state.otaConfig.reset()
this.$store.commit('SET_TARGETS', [])
this.$store.commit('SET_SOURCES', [])

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
const baseURL = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:8000';
const baseURL = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:5000';
console.log(`Build mode: ${process.env.NODE_ENV}, API base url ${baseURL}`);
@@ -23,8 +23,13 @@ export default {
getJobById(id) {
return apiClient.get("/check/" + id)
},
getFileList(path) {
return apiClient.get("/file" + path)
async getBuildList() {
let resp = await apiClient.get("/file");
return resp.data || [];
},
async reconstructBuildList() {
let resp = await apiClient.get("/reconstruct_build_list");
return resp.data;
},
uploadTarget(file, onUploadProgress) {
let formData = new FormData()
@@ -37,12 +42,15 @@ export default {
},
async postInput(input, id) {
try {
const response = await apiClient.post(
'/run/' + id, input)
return response
} catch (err) {
console.log('err:', err)
return
let resp = await apiClient.post(
'/run/' + id, JSON.stringify(input));
return resp.data;
} catch (error) {
if (error.response.data) {
return error.response.data;
} else {
throw error;
}
}
}
}
}

View File

@@ -89,6 +89,7 @@ export class OTAConfiguration {
let jsonOptions = Object.assign({}, this)
jsonOptions.target = targetBuild
jsonOptions.incremental = incrementalSource
jsonOptions.isIncremental = !!incrementalSource;
jsonOptions.id = uuid.v1()
for (let flag of OTAExtraFlags) {
if (jsonOptions[flag.key]) {
@@ -97,8 +98,8 @@ export class OTAConfiguration {
}
}
}
let response = await ApiServices.postInput(jsonOptions, jsonOptions.id)
return response.data
let data = await ApiServices.postInput(jsonOptions, jsonOptions.id)
return data;
}
/**

View File

@@ -16,21 +16,21 @@
<v-divider class="my-5" />
<div>
<h3>STDERR</h3>
<div
<pre
ref="stderr"
class="stderr"
>
{{ job.stderr }}
<p ref="stderrBottom" />
</div>
</pre>
<h3>STDOUT</h3>
<div
<pre
ref="stdout"
class="stdout"
>
{{ job.stdout }}
<p ref="stdoutBottom" />
</div>
</pre>
</div>
<v-divider class="my-5" />
<div class="download">

View File

@@ -3,21 +3,6 @@
v-if="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
block
@click="updateStatus"
@@ -27,20 +12,17 @@
</template>
<script>
import JobDisplay from '@/components/JobDisplay.vue'
import ApiService from '../services/ApiService.js'
import OTAJobTable from '@/components/OTAJobTable.vue'
export default {
name: 'JobList',
components: {
JobDisplay,
OTAJobTable
},
data() {
return {
jobs: null,
overStatus: new Map()
}
},
created (){
@@ -55,9 +37,6 @@ export default {
console.log(err);
}
},
mouseOver(id, status) {
this.overStatus.set(id, status)
}
}
}

View File

@@ -8,6 +8,10 @@ import re
import json
class BuildFileInvalidError(Exception):
pass
@dataclass
class BuildInfo:
"""
@@ -34,29 +38,26 @@ class BuildInfo:
else:
return ''
build = zipfile.ZipFile(self.path)
try:
with build.open('SYSTEM/build.prop', 'r') as build_prop:
raw_info = build_prop.readlines()
pattern_id = re.compile(b'(?<=ro\.build\.id\=).+')
pattern_version = re.compile(
b'(?<=ro\.build\.version\.incremental\=).+')
pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+')
self.build_id = extract_info(
pattern_id, raw_info).decode('utf-8')
self.build_version = extract_info(
pattern_version, raw_info).decode('utf-8')
self.build_flavor = extract_info(
pattern_flavor, raw_info).decode('utf-8')
except KeyError:
pass
try:
with build.open('META/ab_partitions.txt', 'r') as partition_info:
raw_info = partition_info.readlines()
for line in raw_info:
self.partitions.append(line.decode('utf-8').rstrip())
except KeyError:
pass
with zipfile.ZipFile(self.path) as build:
try:
with build.open('SYSTEM/build.prop', 'r') as build_prop:
raw_info = build_prop.readlines()
pattern_id = re.compile(b'(?<=ro\.build\.id\=).+')
pattern_version = re.compile(
b'(?<=ro\.build\.version\.incremental\=).+')
pattern_flavor = re.compile(b'(?<=ro\.build\.flavor\=).+')
self.build_id = extract_info(
pattern_id, raw_info).decode('utf-8')
self.build_version = extract_info(
pattern_version, raw_info).decode('utf-8')
self.build_flavor = extract_info(
pattern_flavor, raw_info).decode('utf-8')
with build.open('META/ab_partitions.txt', 'r') as partition_info:
raw_info = partition_info.readlines()
for line in raw_info:
self.partitions.append(line.decode('utf-8').rstrip())
except KeyError as e:
raise BuildFileInvalidError("Invalid build due to " + str(e))
def to_sql_form_dict(self):
"""
@@ -79,12 +80,16 @@ class TargetLib:
"""
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
"""
self.path = path
with sqlite3.connect(self.path) as connect:
self.working_dir = working_dir
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.execute("""
CREATE TABLE if not exists Builds (
@@ -107,7 +112,12 @@ class TargetLib:
"""
build_info = BuildInfo(filename, path, int(time.time()))
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.execute("""
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)
""", 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
Args:
path: a directory
"""
if os.path.isdir(path):
builds_name = os.listdir(path)
build_dir = self.working_dir
if os.path.isdir(build_dir):
builds_name = os.listdir(build_dir)
for build_name in builds_name:
if build_name.endswith(".zip"):
self.new_build(build_name, os.path.join(path, build_name))
elif os.path.isfile(path) and path.endswith(".zip"):
self.new_build(os.path.split(path)[-1], path)
path = os.path.join(build_dir, build_name)
if build_name.endswith(".zip") and zipfile.is_zipfile(path):
self.new_build(build_name, 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()
def sql_to_buildinfo(self, row):
@@ -147,7 +159,7 @@ class TargetLib:
A list of build_info, each of which is an object:
(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.execute("""
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions
@@ -161,7 +173,7 @@ class TargetLib:
A build_info, which is an object:
(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.execute("""
SELECT FileName, Path, UploadTime, BuildID, BuildVersion, BuildFlavor, Partitions

View File

@@ -182,7 +182,7 @@ class TestProcessesManagement(unittest.TestCase):
def setUp(self):
if os.path.isfile('test_process.db'):
self.tearDown()
self.processes = ProcessesManagement(path='test_process.db')
self.processes = ProcessesManagement(db_path='test_process.db')
testcase_job_info = TestJobInfo()
testcase_job_info.setUp()
self.test_job_info = testcase_job_info.setup_job(incremental='target/source.zip')

View File

@@ -0,0 +1,12 @@
const path = require('path')
module.exports = {
configureWebpack: {
resolve: {
symlinks: false,
alias: {
vue: path.resolve('./node_modules/vue')
}
}
}
}

View File

@@ -33,7 +33,6 @@ import json
import cgi
import os
import stat
import sys
import zipfile
LOCAL_ADDRESS = '0.0.0.0'
@@ -78,17 +77,17 @@ class RequestHandler(CORSSimpleHTTPHandler):
self.wfile.write(
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/':
file_list = target_lib.get_builds()
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]
self._set_response(type='application/json')
self.wfile.write(
json.dumps(builds_info).encode()
)
logging.info(
logging.debug(
"GET request:\nPath:%s\nHeaders:\n%s\nBody:\n%s\n",
str(self.path), str(self.headers), file_list
)
@@ -115,12 +114,16 @@ class RequestHandler(CORSSimpleHTTPHandler):
post_data = json.loads(self.rfile.read(content_length))
try:
jobs.ota_generate(post_data, id=str(self.path[5:]))
self._set_response(code=201)
self.wfile.write(
"ota generator start running".encode('utf-8'))
except SyntaxError:
self.send_error(400)
logging.info(
self._set_response(code=200)
self.send_header("Content-Type", 'application/json')
self.wfile.write(json.dumps(
{"success": True, "msg": "OTA Generator started running"}).encode())
except Exception as e:
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",
str(self.path), str(self.headers),
json.dumps(post_data)
@@ -144,7 +147,8 @@ class RequestHandler(CORSSimpleHTTPHandler):
file_length -= len(self.rfile.readline())
BUFFER_SIZE = 1024*1024
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)
target_lib.new_build(self.path[6:], file_name)
self._set_response(code=201)
@@ -171,17 +175,17 @@ def run_server(SeverClass=ThreadedHTTPServer, HandlerClass=RequestHandler, port=
except KeyboardInterrupt:
pass
server_instance.server_close()
logging.basicConfig(level=logging.DEBUG)
logging.info('Server has been turned off.')
if __name__ == '__main__':
from sys import argv
logging.basicConfig(level=logging.DEBUG)
print(argv)
logging.basicConfig(level=logging.INFO)
EXTRACT_DIR = None
if os.path.exists("otatools.zip"):
logging.info("Found otatools.zip, extracting...")
EXTRACT_DIR = "./"
EXTRACT_DIR = "/tmp/otatools-" + str(os.getpid())
os.makedirs(EXTRACT_DIR, exist_ok=True)
with zipfile.ZipFile("otatools.zip", "r") as zfp:
zfp.extractall(EXTRACT_DIR)
@@ -189,19 +193,14 @@ if __name__ == '__main__':
bin_dir = os.path.join(EXTRACT_DIR, "bin")
for filename in os.listdir(bin_dir):
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("PATH: %s", os.environ["PATH"])
if not os.path.isdir('target'):
os.mkdir('target', 755)
if not os.path.isdir('output'):
os.mkdir('output', 755)
target_lib = TargetLib()
jobs = ProcessesManagement()
try:
if len(argv) == 2:
run_server(port=int(argv[1]))
else:
run_server()
except KeyboardInterrupt:
sys.exit(0)
jobs = ProcessesManagement(otatools_dir=EXTRACT_DIR)
if len(argv) == 2:
run_server(port=int(argv[1]))
else:
run_server()