Simplify apk pipe handling, eliminate InstallItem

This commit is contained in:
Disassembler 2019-02-15 22:24:50 +01:00
parent 166e5b2ece
commit bbdd91cd77
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
4 changed files with 28 additions and 45 deletions

View File

@ -57,7 +57,7 @@ class ActionQueue:
# If the action finished without errors, restore nominal state by deleting the item from action list # If the action finished without errors, restore nominal state by deleting the item from action list
self.clear_action(item.key) self.clear_action(item.key)
except BaseException as e: except BaseException as e:
# If the action failed, store the exception and leave it in the list form manual clearance # If the action failed, store the exception and leave it in the list for manual clearance
with self.lock: with self.lock:
item.data = e item.data = e

View File

@ -1,20 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import math
import os import os
import requests import requests
import subprocess import subprocess
import time import time
class InstallItem:
def __init__(self):
self.bytes_total = 1
self.bytes_downloaded = 0
@property
def percent_downloaded(self):
return round(self.bytes_downloaded / self.bytes_total * 100)
class AppMgr: class AppMgr:
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
@ -64,26 +56,22 @@ class AppMgr:
def install_app(self, item): def install_app(self, item):
# Main installation function. Wrapper for installation via native package manager # Main installation function. Wrapper for installation via native package manager
item.data = InstallItem() item.data = 0
# Alpine apk provides machine-readable progress in bytes_downloaded/bytes_total format output to file descriptor of choice # Alpine apk provides machine-readable progress in bytes_downloaded/bytes_total format output to file descriptor of choice, so create a pipe for it
pipe_rfd, pipe_wfd = os.pipe() pipe_rfd, pipe_wfd = os.pipe()
with subprocess.Popen(['apk', '--progress-fd', str(pipe_wfd), '--no-cache', 'add', 'vm-{}@vm'.format(item.key)], pass_fds=[pipe_wfd]) as p: with os.fdopen(pipe_rfd) as pipe_rf:
while p.poll() == None: with subprocess.Popen(['apk', '--progress-fd', str(pipe_wfd), '--no-cache', 'add', 'vm-{}@vm'.format(item.key)], pass_fds=[pipe_wfd]) as p:
time.sleep(0.1) # Close write pipe for vmmgr to not block the pipe once apk finishes
# Read pipe os.close(pipe_wfd)
data = b'' while p.poll() == None:
while True: # Wait for line end or EOF in read pipe and process it
chunk = os.read(pipe_rfd, 8192) data = pipe_rf.readline()
data += chunk if data:
if len(chunk) < 8192: progress = data.rstrip().split('/')
break item.data = math.floor(int(progress[0]) / int(progress[1]) * 100)
# Parse last apk progress line # If the apk command didn't finish with returncode 0, raise an exception
progress = data.decode().splitlines()[-1].split('/') if p.returncode:
item.data.bytes_downloaded = progress[0] raise CalledProcessError(p.returncode, cmd)
item.data.bytes_total = progress[1]
# Close pipe
os.close(pipe_rfd)
os.close(pipe_wfd)
def uninstall_app(self, item): def uninstall_app(self, item):
# Main uninstallation function. Wrapper for uninstallation via native package manager # Main uninstallation function. Wrapper for uninstallation via native package manager
@ -93,16 +81,18 @@ class AppMgr:
self.update_app_autostart(app, False) self.update_app_autostart(app, False)
if name in self.conf['apps']: if name in self.conf['apps']:
del self.conf['apps'][name] del self.conf['apps'][name]
subprocess.run(['apk', '--no-cache', 'del', 'vm-{}@vm'.format(app)]) subprocess.run(['apk', '--no-cache', 'del', 'vm-{}@vm'.format(app)], check=True)
def fetch_online_packages(self, repo_conf): def fetch_online_packages(self, repo_conf):
# Fetches list of online packages # Fetches list of online packages
auth = (repo_conf['user'], repo_conf['pwd']) if repo_conf['user'] else None auth = (repo_conf['user'], repo_conf['pwd']) if repo_conf['user'] else None
packages = requests.get('{}/packages.json'.format(repo_conf['url']), auth=auth, timeout=5) try:
if packages.status_code != 200: packages = requests.get('{}/packages.json'.format(repo_conf['url']), auth=auth, timeout=5)
return packages.status_code except:
self.online_packages = json.loads(packages.content) return 0
return 200 if packages.status_code == 200:
self.online_packages = json.loads(packages.content)
return packages.status_code
def get_services_deps(self): def get_services_deps(self):
# Fisrt, build a dictionary of {app: [needs]} # Fisrt, build a dictionary of {app: [needs]}

View File

@ -164,11 +164,8 @@ class WSGIApp:
# Application manager view. # Application manager view.
repo_error = None repo_error = None
repo_conf = self.vmmgr.get_repo_conf() repo_conf = self.vmmgr.get_repo_conf()
try: status = self.appmgr.fetch_online_packages(repo_conf)
status = self.appmgr.fetch_online_packages(repo_conf) if status == 401:
except InvalidSignature:
repo_error = request.session.lang.invalid_packages_signature()
if status in (401, 403):
repo_error = request.session.lang.repo_invalid_credentials() repo_error = request.session.lang.repo_invalid_credentials()
elif status != 200: elif status != 200:
repo_error = request.session.lang.repo_unavailable() repo_error = request.session.lang.repo_unavailable()
@ -212,10 +209,8 @@ class WSGIApp:
status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.package_manager_error()) status = '<span class="error">{}</span> <a href="#" class="app-clear-status">OK</a>'.format(lang.package_manager_error())
actions = None actions = None
else: else:
if item.data.stage == 0: if item.data < 100:
status = '{} ({} %)'.format(lang.status_downloading(), item.data) status = '{} ({} %)'.format(lang.status_downloading(), item.data)
elif item.data.stage == 1:
status = lang.status_installing_deps()
else: else:
status = lang.status_installing() status = lang.status_installing()
elif item.action == self.appmgr.uninstall_app: elif item.action == self.appmgr.uninstall_app:

View File

@ -23,7 +23,6 @@ class WSGILang:
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.', 'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.', 'installation_in_progress': 'Probíhá instalace jiného balíku. Vyčkejte na její dokončení.',
'package_manager_error': 'Došlo k chybě při instalaci aplikace. Zkuste akci opakovat nebo restartuje virtuální stroj.', 'package_manager_error': 'Došlo k chybě při instalaci aplikace. Zkuste akci opakovat nebo restartuje virtuální stroj.',
'invalid_packages_signature': 'Digitální podpis seznamu balíků není platný. Kontaktujte správce distribučního serveru.',
'repo_invalid_credentials': 'Přístupové údaje k distribučnímu serveru nejsou správné.', 'repo_invalid_credentials': 'Přístupové údaje k distribučnímu serveru nejsou správné.',
'repo_unavailable': 'Distribuční server není dostupný. Zkontroluje připojení k síti', 'repo_unavailable': 'Distribuční server není dostupný. Zkontroluje připojení k síti',
'bad_password': 'Nesprávné heslo', 'bad_password': 'Nesprávné heslo',
@ -38,7 +37,6 @@ class WSGILang:
'status_stopping': 'Zastavuje se', 'status_stopping': 'Zastavuje se',
'status_stopped': 'Zastavena', 'status_stopped': 'Zastavena',
'status_downloading': 'Stahuje se', 'status_downloading': 'Stahuje se',
'status_installing_deps': 'Instalují se závislosti',
'status_installing': 'Instaluje se', 'status_installing': 'Instaluje se',
'status_uninstalling': 'Odinstalovává se', 'status_uninstalling': 'Odinstalovává se',
'status_not_installed': 'Není nainstalována', 'status_not_installed': 'Není nainstalována',