refactoring async work

This commit is contained in:
Vinícius Moreira
2019-07-24 16:01:44 -03:00
committed by GitHub
parent e35b515b5b
commit c7a111174a
10 changed files with 263 additions and 222 deletions

View File

@@ -1,11 +1,9 @@
import time
from abc import ABC, abstractmethod
from argparse import Namespace
from threading import Lock
from typing import List, Dict
from fpakman.core.disk import DiskCacheLoader, DiskCacheLoaderFactory
from fpakman.core.model import Application
from fpakman.core.model import Application, ApplicationUpdate
from fpakman.core.system import FpakmanProcess
@@ -82,6 +80,10 @@ class ApplicationManager(ABC):
"""
pass
@abstractmethod
def list_updates(self) -> List[ApplicationUpdate]:
pass
class GenericApplicationManager(ApplicationManager):
@@ -90,7 +92,6 @@ class GenericApplicationManager(ApplicationManager):
self.managers = managers
self.map = {m.get_app_type(): m for m in self.managers}
self.disk_loader_factory = disk_loader_factory
self.lock_read = Lock()
self._enabled_map = {} if app_args.check_packaging_once else None
self.prepare()
@@ -131,8 +132,6 @@ class GenericApplicationManager(ApplicationManager):
return man.is_enabled()
def search(self, word: str, disk_loader: DiskCacheLoader = None) -> Dict[str, List[Application]]:
self.lock_read.acquire()
try:
res = {'installed': [], 'new': []}
norm_word = word.strip().lower()
@@ -155,13 +154,8 @@ class GenericApplicationManager(ApplicationManager):
res[key] = self._sort(res[key], norm_word)
return res
finally:
self.lock_read.release()
def read_installed(self, disk_loader: DiskCacheLoader = None) -> List[Application]:
self.lock_read.acquire()
try:
installed = []
disk_loader = None
@@ -180,8 +174,6 @@ class GenericApplicationManager(ApplicationManager):
installed.sort(key=lambda a: a.base_data.name.lower())
return installed
finally:
self.lock_read.release()
def can_downgrade(self):
return True
@@ -260,8 +252,17 @@ class GenericApplicationManager(ApplicationManager):
return man.refresh(app, root_password)
def prepare(self):
if self.managers:
for man in self.managers:
if self._is_enabled(man):
man.prepare()
def list_updates(self) -> List[ApplicationUpdate]:
updates = []
if self.managers:
for man in self.managers:
if self._is_enabled(man):
updates.extend(man.list_updates())
return updates

View File

@@ -9,8 +9,8 @@ from fpakman.core.controller import ApplicationManager
from fpakman.core.disk import DiskCacheLoader
from fpakman.core.flatpak import flatpak
from fpakman.core.flatpak.model import FlatpakApplication
from fpakman.core.flatpak.worker import FlatpakAsyncDataLoader
from fpakman.core.model import ApplicationData
from fpakman.core.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader
from fpakman.core.model import ApplicationData, ApplicationUpdate
from fpakman.core.system import FpakmanProcess
from fpakman.util.cache import Cache
@@ -148,3 +148,33 @@ class FlatpakManager(ApplicationManager):
def prepare(self):
flatpak.set_default_remotes()
def list_updates(self) -> List[ApplicationUpdate]:
updates = []
installed = flatpak.list_installed(extra_fields=False)
if installed:
available_updates = flatpak.list_updates_as_str()
if available_updates:
loaders = None
for app_json in installed:
if app_json['id'] in available_updates:
loader = FlatpakUpdateLoader(app=app_json, http_session=self.http_session)
loader.start()
if loaders is None:
loaders = []
loaders.append(loader)
if loaders:
for loader in loaders:
loader.join()
app = loader.app
updates.append(ApplicationUpdate(app_id='{}:{}'.format(app['id'], app['branch']),
app_type='flatpak',
version=app.get('version')))
return updates

View File

@@ -1,6 +1,6 @@
import re
import subprocess
from typing import List
from typing import List, Set
from fpakman.core import system
from fpakman.core.exception import NoInternetException
@@ -8,7 +8,7 @@ from fpakman.core.exception import NoInternetException
BASE_CMD = 'flatpak'
def app_str_to_json(line: str, version: str) -> dict:
def app_str_to_json(line: str, version: str, extra_fields: bool = True) -> dict:
app_array = line.split('\t')
@@ -17,18 +17,21 @@ def app_str_to_json(line: str, version: str) -> dict:
'id': app_array[1],
'version': app_array[2],
'branch': app_array[3]}
elif '1.0' <= version < '1.1':
app = {'ref': app_array[0], 'options': app_array[1]}
ref_data = app['ref'].split('/')
app['id'] = ref_data[0]
app['arch'] = ref_data[1]
app['branch'] = ref_data[2]
app['name'] = ref_data[0].split('.')[-1]
app['version'] = None
elif '1.0' <= version < '1.1':
ref_data = app_array[0].split('/')
app = {'id': ref_data[0],
'arch': ref_data[1],
'name': ref_data[0].split('.')[-1],
'ref': app_array[0],
'options': app_array[1],
'branch': ref_data[2],
'version': None}
elif '1.2' <= version < '1.3':
app = {'name': app_array[1].strip().split('.')[-1],
'id': app_array[1],
app = {'id': app_array[1],
'name': app_array[1].strip().split('.')[-1],
'version': app_array[2],
'branch': app_array[3],
'arch': app_array[4],
@@ -36,6 +39,7 @@ def app_str_to_json(line: str, version: str) -> dict:
else:
raise Exception('Unsupported version')
if extra_fields:
extra_fields = get_app_info_fields(app['id'], app['branch'], ['origin', 'arch', 'ref', 'commit'], check_runtime=True)
app.update(extra_fields)
@@ -81,13 +85,13 @@ def get_app_info(app_id: str, branch: str):
return system.run_cmd('{} info {} {}'.format(BASE_CMD, app_id, branch))
def list_installed() -> List[dict]:
def list_installed(extra_fields: bool = True) -> List[dict]:
apps_str = system.run_cmd('{} list'.format(BASE_CMD))
if apps_str:
version = get_version()
app_lines = apps_str.split('\n')
return [app_str_to_json(line, version) for line in app_lines if line]
return [app_str_to_json(line, version, extra_fields=extra_fields) for line in app_lines if line]
return []
@@ -215,4 +219,3 @@ def install_and_stream(app_id: str, origin: str):
def set_default_remotes():
system.run_cmd('flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo')

View File

@@ -1,9 +1,11 @@
import time
import traceback
from threading import Thread
from colorama import Fore
from fpakman.core.controller import ApplicationManager
from fpakman.core.flatpak import flatpak
from fpakman.core.flatpak.constants import FLATHUB_API_URL, FLATHUB_URL
from fpakman.core.flatpak.model import FlatpakApplication
from fpakman.core.model import ApplicationStatus
@@ -82,3 +84,41 @@ class FlatpakAsyncDataLoader(AsyncDataLoader):
http_session=self.http_session,
timeout=self.timeout,
app=self.app)
class FlatpakUpdateLoader(Thread):
def __init__(self, app: dict, http_session, attempts: int = 2, timeout: int = 20):
super(FlatpakUpdateLoader, self).__init__(daemon=True)
self.app = app
self.http_session = http_session
self.attempts = attempts
self.timeout = timeout
def run(self):
if self.app.get('ref') is None:
self.app.update(flatpak.get_app_info_fields(self.app['id'], self.app['branch'], fields=['ref'], check_runtime=True))
else:
self.app['runtime'] = self.app['ref'].startswith('runtime/')
if not self.app['runtime']:
current_attempts = 0
while current_attempts < self.attempts:
current_attempts += 1
try:
res = self.http_session.get('{}/apps/{}'.format(FLATHUB_API_URL, self.app['id']), timeout=self.timeout)
if res.status_code == 200 and res.text:
data = res.json()
if data.get('currentReleaseVersion'):
self.app['version'] = data['currentReleaseVersion']
break
except:
traceback.print_exc()
time.sleep(0.5)

View File

@@ -86,3 +86,14 @@ class Application(ABC):
def __str__(self):
return '{} (id={}, name={})'.format(self.__class__.__name__, self.base_data.id, self.base_data.name)
class ApplicationUpdate:
def __init__(self, app_id: str, version: str, app_type: str):
self.id = app_id
self.version = version
self.type = app_type
def __str__(self):
return '{} (id={}, type={}, new_version={})'.format(self.__class__.__name__, self.id, self.type, self.type)

View File

@@ -7,7 +7,7 @@ from typing import Dict, List
from fpakman.core import disk
from fpakman.core.controller import ApplicationManager
from fpakman.core.disk import DiskCacheLoader
from fpakman.core.model import ApplicationData, Application
from fpakman.core.model import ApplicationData, Application, ApplicationUpdate
from fpakman.core.snap import snap
from fpakman.core.snap.model import SnapApplication
from fpakman.core.snap.worker import SnapAsyncDataLoader
@@ -131,3 +131,6 @@ class SnapManager(ApplicationManager):
def prepare(self):
pass
def list_updates(self) -> List[ApplicationUpdate]:
return []

View File

@@ -119,9 +119,6 @@ class AppsTable(QTableWidget):
menu_row.exec_()
def fill_async_data(self):
self.lock_async_data.acquire()
if self.window.apps:
for idx, app_v in enumerate(self.window.apps):
@@ -142,8 +139,6 @@ class AppsTable(QTableWidget):
self.window.resize_and_center()
self.lock_async_data.release()
def get_selected_app(self) -> ApplicationView:
return self.window.apps[self.currentRow()]
@@ -202,9 +197,9 @@ class AppsTable(QTableWidget):
if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
self.window.manager.cache_to_disk(app=app.model, icon_bytes=icon_data['bytes'], only_icon=True)
def update_apps(self, app_views: List[ApplicationView]):
self.setEnabled(True)
def update_apps(self, app_views: List[ApplicationView], update_check_enabled: bool = True):
self.setRowCount(len(app_views) if app_views else 0)
self.setEnabled(True)
if app_views:
for idx, app_v in enumerate(app_views):
@@ -214,7 +209,11 @@ class AppsTable(QTableWidget):
self._set_col_type(idx, app_v)
self._set_col_installed(idx, app_v)
col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update) if app_v.model.update else None
col_update = None
if update_check_enabled and app_v.model.update:
col_update = UpdateToggleButton(app_v, self.window, self.window.locale_keys, app_v.model.update)
self.setCellWidget(idx, 5, col_update)
def _set_col_installed(self, idx: int, app_v: ApplicationView):

View File

@@ -6,9 +6,10 @@ from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from fpakman import __app_name__
from fpakman.core import resource, system
from fpakman.core.controller import ApplicationManager
from fpakman.core.model import Application
from fpakman.core.model import ApplicationUpdate
from fpakman.util.cache import Cache
from fpakman.view.qt.about import AboutDialog
from fpakman.view.qt.window import ManageWindow
@@ -27,9 +28,7 @@ class UpdateCheck(QThread):
while True:
apps = self.manager.read_installed()
updates = [app for app in apps if app.update]
updates = self.manager.list_updates()
if updates:
self.signal.emit(updates)
@@ -66,30 +65,32 @@ class TrayIcon(QSystemTrayIcon):
self.setContextMenu(self.menu)
self.manage_window = None
self.dialog_about = None
self.check_thread = UpdateCheck(check_interval=check_interval, manager=self.manager)
self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start()
self.dialog_about = None
self.last_updates = set()
self.update_notification = update_notification
self.lock_notify = Lock()
self.activated.connect(self.handle_click)
self.set_default_tooltip()
def set_default_tooltip(self):
self.setToolTip('{} ({})'.format(self.locale_keys['manage_window.title'], __app_name__))
def handle_click(self, reason):
if reason == self.Trigger:
self.show_manage_window()
def notify_updates(self, updates: List[Application]):
def notify_updates(self, updates: List[ApplicationUpdate]):
self.lock_notify.acquire()
try:
if len(updates) > 0:
update_keys = {'{}:{}'.format(app.base_data.id, app.base_data.version) for app in updates}
update_keys = {'{}:{}:{}'.format(up.type, up.id, up.version) for up in updates}
new_icon = self.icon_update
@@ -103,7 +104,7 @@ class TrayIcon(QSystemTrayIcon):
else:
new_icon = self.icon_default
self.setToolTip(None)
self.set_default_tooltip()
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
self.setIcon(new_icon)

View File

@@ -302,6 +302,8 @@ class VerifyModels(QThread):
if stop_at <= datetime.utcnow():
break
time.sleep(0.1)
self.apps = None

View File

@@ -1,6 +1,5 @@
import operator
from functools import reduce
from threading import Lock
from typing import List, Set
from PyQt5.QtCore import QEvent, Qt
@@ -31,7 +30,6 @@ class ManageWindow(QWidget):
self.locale_keys = locale_keys
self.manager = manager
self.tray_icon = tray_icon
self.thread_lock = Lock()
self.working = False # restrict the number of threaded actions
self.apps = []
self.label_flatpak = None
@@ -42,7 +40,6 @@ class ManageWindow(QWidget):
self.icon_flathub = QIcon(resource.get_path('img/logo.svg'))
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
self.setWindowTitle(locale_keys['manage_window.title'])
self.setWindowIcon(self.icon_flathub)
self.layout = QVBoxLayout()
@@ -111,7 +108,7 @@ class ManageWindow(QWidget):
self.bt_upgrade.setIcon(QIcon(resource.get_path('img/update_green.svg')))
self.bt_upgrade.setEnabled(False)
self.bt_upgrade.clicked.connect(self.update_selected)
toolbar.addWidget(self.bt_upgrade)
self.ref_bt_upgrade = toolbar.addWidget(self.bt_upgrade)
self.layout.addWidget(toolbar)
@@ -235,25 +232,6 @@ class ManageWindow(QWidget):
self.hide()
self._handle_console_option(False)
def _acquire_lock(self):
self.thread_lock.acquire()
if not self.working:
self.working = True
self.thread_lock.release()
return self.working
def _release_lock(self):
self.thread_lock.acquire()
if self.working:
self.working = False
self.thread_lock.release()
def _handle_console(self, checked: bool):
if checked:
@@ -271,8 +249,6 @@ class ManageWindow(QWidget):
self.textarea_output.hide()
def refresh_apps(self, keep_console: bool = True):
if self._acquire_lock():
self.filter_types.clear()
self.input_search.clear()
@@ -282,18 +258,15 @@ class ManageWindow(QWidget):
self.ref_checkbox_updates.setVisible(False)
self.ref_checkbox_only_apps.setVisible(False)
self._begin_action(self.locale_keys['manage_window.status.refreshing'], clear_filters=True)
self.thread_refresh.start()
def _finish_refresh_apps(self, apps: List[Application]):
self.update_apps(apps)
self.finish_action()
self.ref_checkbox_only_apps.setVisible(True)
self._release_lock()
self.ref_bt_upgrade.setVisible(True)
def uninstall_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('uninstall', self.table_apps.get_selected_app().model)
@@ -301,7 +274,6 @@ class ManageWindow(QWidget):
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
self._release_lock()
return
self._handle_console_option(True)
@@ -312,8 +284,6 @@ class ManageWindow(QWidget):
self.thread_uninstall.start()
def refresh(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('refresh', self.table_apps.get_selected_app().model)
@@ -321,7 +291,6 @@ class ManageWindow(QWidget):
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
self._release_lock()
return
self._handle_console_option(True)
@@ -333,7 +302,6 @@ class ManageWindow(QWidget):
def _finish_uninstall(self, success: bool):
self.finish_action()
self._release_lock()
if success:
if self._can_notify_user():
@@ -353,7 +321,6 @@ class ManageWindow(QWidget):
def _finish_downgrade(self, success: bool):
self.finish_action()
self._release_lock()
if success:
if self._can_notify_user():
@@ -365,17 +332,16 @@ class ManageWindow(QWidget):
if self._can_notify_user():
system.notify_user(self.locale_keys['notification.downgrade.failed'])
self.change_update_state(notify_tray=False)
self.change_update_state()
self.checkbox_console.setChecked(True)
def _finish_refresh(self, success: bool):
self.finish_action()
self._release_lock()
if success:
self.refresh_apps()
else:
self.change_update_state(notify_tray=False)
self.change_update_state()
self.checkbox_console.setChecked(True)
def apply_filters(self):
@@ -399,11 +365,9 @@ class ManageWindow(QWidget):
self.table_apps.change_headers_policy()
self.resize_and_center(accept_lower_width=visible_apps > 0)
def change_update_state(self, notify_tray: bool = True, change_filters: bool = True):
def change_update_state(self, change_filters: bool = True):
enable_bt_update = False
app_updates, library_updates = 0, 0
app_updates, library_updates, not_installed = 0, 0, 0
for app_v in self.apps:
if app_v.model.update:
@@ -412,14 +376,18 @@ class ManageWindow(QWidget):
else:
app_updates += 1
if not app_v.model.installed:
not_installed += 1
for app_v in self.apps:
if app_v.visible and app_v.update_checked:
if not_installed == 0 and app_v.visible and app_v.update_checked:
enable_bt_update = True
break
self.bt_upgrade.setEnabled(enable_bt_update)
total_updates = app_updates + library_updates
if total_updates > 0:
self.label_updates.setPixmap(QPixmap(resource.get_path('img/exclamation.svg')).scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.label_updates.setToolTip('{}: {} ( {} {} | {} {} )'.format(self.locale_keys['manage_window.label.updates'],
@@ -429,6 +397,7 @@ class ManageWindow(QWidget):
library_updates,
self.locale_keys['others'].lower()))
if not_installed == 0:
if not self.ref_checkbox_updates.isVisible():
self.ref_checkbox_updates.setVisible(True)
@@ -442,9 +411,6 @@ class ManageWindow(QWidget):
self.ref_checkbox_updates.setVisible(False)
self.label_updates.setPixmap(QPixmap())
if notify_tray:
self.tray_icon.notify_updates([app.model for app in self.apps if app.model.update])
def centralize(self):
geo = self.frameGeometry()
screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos())
@@ -452,7 +418,7 @@ class ManageWindow(QWidget):
geo.moveCenter(center_point)
self.move(geo.topLeft())
def update_apps(self, apps: List[Application]):
def update_apps(self, apps: List[Application], update_check_enabled: bool = True):
self.apps = []
napps = 0 # number of apps (not libraries)
@@ -473,7 +439,7 @@ class ManageWindow(QWidget):
self.checkbox_only_apps.setChecked(True)
self._update_type_filters(available_types)
self.table_apps.update_apps(self.apps)
self.table_apps.update_apps(self.apps, update_check_enabled=update_check_enabled)
self.apply_filters()
self.change_update_state()
self.resize_and_center()
@@ -510,8 +476,6 @@ class ManageWindow(QWidget):
self.centralize()
def update_selected(self):
if self._acquire_lock():
if self.apps:
to_update = [app_v for app_v in self.apps if app_v.visible and app_v.update_checked]
@@ -525,7 +489,6 @@ class ManageWindow(QWidget):
def _finish_update_selected(self, success: bool, updated: int):
self.finish_action()
self._release_lock()
if success:
if self._can_notify_user():
@@ -581,8 +544,6 @@ class ManageWindow(QWidget):
self.checkbox_updates.setEnabled(True)
def downgrade_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('downgrade', self.table_apps.get_selected_app().model)
@@ -590,7 +551,6 @@ class ManageWindow(QWidget):
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
self._release_lock()
return
self._handle_console_option(True)
@@ -601,8 +561,6 @@ class ManageWindow(QWidget):
self.thread_downgrade.start()
def get_app_info(self, app: dict):
if self._acquire_lock():
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.info'])
@@ -610,7 +568,6 @@ class ManageWindow(QWidget):
self.thread_get_info.start()
def get_app_history(self, app: dict):
if self._acquire_lock():
self._handle_console_option(False)
self._begin_action(self.locale_keys['manage_window.status.history'])
@@ -618,14 +575,12 @@ class ManageWindow(QWidget):
self.thread_get_history.start()
def _finish_get_info(self, app_info: dict):
self._release_lock()
self.finish_action()
self.change_update_state()
dialog_info = InfoDialog(app_info, self.table_apps.get_selected_app_icon(), self.locale_keys, self.table_apps.get_selected_app().model.get_type(), self.screen_size)
dialog_info.exec_()
def _finish_get_history(self, app: dict):
self._release_lock()
self.finish_action()
self.change_update_state()
@@ -641,7 +596,7 @@ class ManageWindow(QWidget):
word = self.input_search.text().strip()
if word and self._acquire_lock():
if word:
self._handle_console_option(False)
self.ref_checkbox_only_apps.setVisible(False)
self.ref_checkbox_updates.setVisible(False)
@@ -651,13 +606,11 @@ class ManageWindow(QWidget):
self.thread_search.start()
def _finish_search(self, apps_found: List[Application]):
self._release_lock()
self.finish_action()
self.update_apps(apps_found)
self.ref_bt_upgrade.setVisible(False)
self.update_apps(apps_found, update_check_enabled=False)
def install_app(self, app: ApplicationView):
if self._acquire_lock():
pwd = None
requires_root = self.manager.requires_root('install', self.table_apps.get_selected_app().model)
@@ -665,7 +618,6 @@ class ManageWindow(QWidget):
pwd, ok = ask_root_password(self.locale_keys)
if not ok:
self._release_lock()
return
self._handle_console_option(True)
@@ -678,7 +630,6 @@ class ManageWindow(QWidget):
def _finish_install(self, success: bool):
self.input_search.setText('')
self.finish_action()
self._release_lock()
if success:
if self._can_notify_user():