AUR bearhub: fix stale source cache collision (pkgrel=10)

Use tag-specific tarball name bearhub-0.10.7-bearhub.6.tar.gz so makepkg
and AUR helpers do not reuse an outdated bearhub-0.10.7.tar.gz from cache.
Ignore makepkg artifacts under packaging/aur/.
This commit is contained in:
Sebastian Palencsar
2026-06-27 10:02:45 +02:00
parent c56b8a2473
commit da377aecdc
198 changed files with 4543 additions and 4644 deletions

View File

@@ -1,50 +0,0 @@
from abc import ABC, abstractmethod
from typing import Set, Optional
class MemoryCache(ABC):
"""
Represents a memory cache.
"""
@abstractmethod
def is_enabled(self):
pass
@abstractmethod
def add(self, key: str, val: object):
pass
@abstractmethod
def add_non_existing(self, key: str, val: object):
pass
@abstractmethod
def get(self, key: str):
pass
@abstractmethod
def delete(self, key):
pass
@abstractmethod
def keys(self) -> Set[str]:
pass
@abstractmethod
def clean_expired(self):
pass
class MemoryCacheFactory(ABC):
"""
Instantiate new memory cache instances.
"""
@abstractmethod
def new(self, expiration: Optional[int]) -> MemoryCache:
"""
:param expiration: expiration time for the cache keys in seconds. Use -1 to disable this feature.
:return:
"""
pass

View File

@@ -1,75 +0,0 @@
import logging
import sys
from typing import Optional, Dict
from bauh.api.abstract.cache import MemoryCacheFactory
from bauh.api.abstract.disk import DiskCacheLoaderFactory
from bauh.api.abstract.download import FileDownloader
from bauh.api.http import HttpClient
from bauh.commons.internet import InternetChecker
from bauh.view.util.translation import I18n
class ApplicationContext:
def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str,
app_version: str, internet_checker: InternetChecker, root_user: bool, screen_width: int = -1,
screen_height: int = -1, suggestions_mapping: Optional[Dict[str, str]] = None):
"""
:param download_icons: if packages icons should be downloaded
:param http_client: a shared instance of http client
:param app_root_dir: GUI root dir
:param i18n: the translation keys
:param cache_factory:
:param disk_loader_factory:
:param logger: a logger instance
:param file_downloader
:param distro
:param app_name
:param app_version
:param internet_checker
:param screen_width
:param screen_height
:param suggestions_mapping
"""
self.download_icons = download_icons
self.http_client = http_client
self.app_root_dir = app_root_dir
self.i18n = i18n
self.cache_factory = cache_factory
self.disk_loader_factory = disk_loader_factory
self.logger = logger
self.file_downloader = file_downloader
self.arch_x86_64 = sys.maxsize > 2**32
self.distro = distro
self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility')
self.app_name = app_name
self.app_version = app_version
self.root_user = root_user
self.root_password = None
self.internet_checker = internet_checker
self.screen_width = screen_width
self.screen_height = screen_height
self._suggestions_mapping = suggestions_mapping
def is_system_x86_64(self):
return self.arch_x86_64
def get_view_path(self):
return self.app_root_dir + '/view'
def is_internet_available(self) -> bool:
return self.internet_checker.is_available()
def get_suggestion_url(self, module: str, default: Optional[str] = None) -> Optional[str]:
if self._suggestions_mapping:
module_split = module.split(f'{self.app_name}.gems.')
if len(module_split) > 1:
gem_name = module_split[1].split('.')[0]
return self._suggestions_mapping.get(gem_name, default)
return default

View File

@@ -1,121 +0,0 @@
import logging
import time
import traceback
from typing import Optional
import requests
import yaml
from bauh.commons import system
from bauh.commons.view_utils import get_human_size_str
class HttpClient:
def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5):
self.max_attempts = max_attempts
self.session = requests.Session()
self.timeout = timeout
self.sleep = sleep
self.logger = logger
def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False,
session: bool = True, stream: bool = False) -> Optional[requests.Response]:
cur_attempts = 1
while cur_attempts <= self.max_attempts:
cur_attempts += 1
try:
args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream}
if params:
args['params'] = params
if headers:
args['headers'] = headers
if ignore_ssl:
args['verify'] = False
if session:
res = self.session.get(url, **args)
else:
res = requests.get(url, **args)
if 200 <= res.status_code < 300:
return res
if single_call:
return res
if self.sleep > 0:
time.sleep(self.sleep)
except Exception as e:
if isinstance(e, requests.exceptions.ConnectionError):
self.logger.error('Internet seems to be off')
raise e
elif isinstance(e, requests.exceptions.TooManyRedirects):
self.logger.warning(f"Too many redirects for GET -> {url}")
raise e
elif e.__class__ in (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema):
self.logger.warning(f"The URL '{url}' has an invalid schema")
raise e
self.logger.error(f"Could not retrieve data from '{url}'")
traceback.print_exc()
continue
self.logger.warning(f"Could not retrieve data from '{url}'")
def get_json(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
return res.json() if res else None
def get_yaml(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
return yaml.safe_load(res.text) if res else None
def get_content_length_in_bytes(self, url: str, session: bool = True) -> Optional[int]:
if not url:
return
params = {'url': url, 'allow_redirects': True, 'stream': True}
try:
if session:
res = self.session.get(**params)
else:
res = requests.get(**params)
except requests.exceptions.ConnectionError:
self.logger.info(f"Internet seems to be off. Could not reach '{url}'")
return
if res.status_code == 200:
size = res.headers.get('Content-Length')
if size:
try:
return int(size)
except Exception:
pass
def get_content_length(self, url: str, session: bool = True) -> Optional[str]:
size = self.get_content_length_in_bytes(url, session)
if size:
return get_human_size_str(size)
def exists(self, url: str, session: bool = True, timeout: int = 5) -> bool:
params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': timeout}
try:
if session:
res = self.session.head(**params)
else:
res = self.session.get(**params)
except requests.exceptions.TooManyRedirects:
self.logger.warning(f"{url} seems to exist, but too many redirects have happened")
return True
return res.status_code in (200, 403)

View File

@@ -1,70 +0,0 @@
import os
import shutil
from getpass import getuser
from pathlib import Path
from bauh import __app_name__
from bauh.api import user
def get_temp_dir(username: str) -> str:
return f'/tmp/{__app_name__}@{username}'
CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}'
CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}'
USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes'
DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications'
TEMP_DIR = get_temp_dir(getuser())
LOGS_DIR = f'{TEMP_DIR}/logs'
AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart'
BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin'
SHARED_FILES_DIR = f'/usr/local/share/{__app_name__}' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}'
def _migrate_dir_if_possible(old_path: str, new_path: str):
if old_path == new_path:
return
if not os.path.exists(old_path):
return
if os.path.exists(new_path):
return
try:
parent = os.path.dirname(new_path)
if parent:
Path(parent).mkdir(parents=True, exist_ok=True)
shutil.move(old_path, new_path)
except Exception:
# The migration is best-effort. If it fails, the app can still start with empty defaults.
pass
def migrate_legacy_paths():
old_app_name = 'bauh'
if old_app_name == __app_name__:
return
if user.is_root():
old_cache = f'/var/cache/{old_app_name}'
old_config = f'/etc/{old_app_name}'
old_shared = f'/usr/local/share/{old_app_name}'
old_temp = f'/tmp/{old_app_name}@{getuser()}'
else:
home = Path.home()
old_cache = f'{home}/.cache/{old_app_name}'
old_config = f'{home}/.config/{old_app_name}'
old_shared = f'{home}/.local/share/{old_app_name}'
old_temp = f'/tmp/{old_app_name}@{getuser()}'
_migrate_dir_if_possible(old_cache, CACHE_DIR)
_migrate_dir_if_possible(old_config, CONFIG_DIR)
_migrate_dir_if_possible(old_shared, SHARED_FILES_DIR)
_migrate_dir_if_possible(old_temp, TEMP_DIR)
migrate_legacy_paths()

View File

@@ -1,6 +0,0 @@
import os
from typing import Optional
def is_root(user_id: Optional[int] = None):
return user_id == 0 if user_id is not None else os.getuid() == 0

View File

@@ -1,102 +0,0 @@
import os
import traceback
from abc import abstractmethod, ABC
from pathlib import Path
from threading import Thread
from typing import Optional
import yaml
from bauh.api.paths import CONFIG_DIR
from bauh.commons import util
def read_config(file_path: str, template: dict, update_file: bool = False, update_async: bool = False) -> dict:
if not os.path.exists(file_path):
Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True)
save_config(template, file_path)
else:
with open(file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config:
util.deep_update(template, local_config)
if update_file:
if update_async:
Thread(target=save_config, args=(template, file_path), daemon=True).start()
else:
save_config(template, file_path)
return template
def save_config(config: dict, file_path: str):
with open(file_path, 'w+') as f:
f.write(yaml.dump(config))
class ConfigManager(ABC):
@abstractmethod
def read_config(self) -> Optional[dict]:
pass
@abstractmethod
def get_default_config(self) -> dict:
pass
@abstractmethod
def is_config_cached(self) -> bool:
pass
def get_config(self) -> dict:
default_config = self.get_default_config()
if default_config:
cached_config = self.read_config()
if cached_config:
self.merge_config(default_config, cached_config)
return default_config
@staticmethod
def merge_config(base_config: dict, current_config: dict):
util.deep_update(base_config, current_config)
@abstractmethod
def save_config(self, config_obj: dict):
pass
class YAMLConfigManager(ConfigManager, ABC):
def __init__(self, config_file_path: str):
self.file_path = config_file_path
def is_config_cached(self) -> bool:
return os.path.exists(self.file_path)
def read_config(self) -> Optional[dict]:
if self.is_config_cached():
with open(self.file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config is not None:
return local_config
def save_config(self, config_obj: dict):
if config_obj:
config_dir = os.path.dirname(self.file_path)
try:
Path(config_dir).mkdir(parents=True, exist_ok=True)
except OSError:
traceback.print_exc()
return
try:
with open(self.file_path, 'w+') as f:
f.write(yaml.dump(config_obj))
except Exception:
traceback.print_exc()

View File

@@ -1,17 +0,0 @@
import socket
class InternetChecker:
def __init__(self, offline: bool):
self.offline = offline
def is_available(self) -> bool:
if self.offline:
return False
try:
socket.gethostbyname("w3.org")
return True
except Exception:
return False

View File

@@ -1,702 +0,0 @@
import shutil
import time
import traceback
from subprocess import Popen, STDOUT
from threading import Thread
from typing import List, Set, Type, Tuple, Dict, Optional, Generator, Callable
from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \
UpgradeRequirement, TransactionResult, SoftwareAction, SettingsView, SettingsController
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher, TaskManager
from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, \
CustomSoftwareAction
from bauh.api.abstract.view import TabGroupComponent, MessageType
from bauh.api.exception import NoInternetException
from bauh.commons.boot import CreateConfigFile
from bauh.commons.html import bold
from bauh.commons.regex import RE_URL
from bauh.commons.util import sanitize_command_input
from bauh.view.core.config import CoreConfigManager
from bauh.view.core.settings import GenericSettingsManager
from bauh.view.core.update import check_for_update
from bauh.view.util import resource
from bauh.view.util.resource import get_path
from bauh.view.util.util import clean_app_files, restart_app
class GenericUpgradeRequirements(UpgradeRequirements):
def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[UpgradeRequirement],
to_upgrade: List[UpgradeRequirement], cannot_upgrade: List[SoftwarePackage],
sub_requirements: Dict[SoftwareManager, UpgradeRequirements]):
super(GenericUpgradeRequirements, self).__init__(to_install=to_install, to_upgrade=to_upgrade,
to_remove=to_remove, cannot_upgrade=cannot_upgrade)
self.sub_requirements = sub_requirements
class GenericSoftwareManager(SoftwareManager, SettingsController):
def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, config: dict,
force_suggestions: bool = False):
super(GenericSoftwareManager, self).__init__(context=context)
self.managers = managers
self.map = {t: m for m in self.managers for t in m.get_managed_types()}
self._available_cache = {} if config['system']['single_dependency_checking'] else None
self.thread_prepare = None
self.i18n = context.i18n
self.disk_loader_factory = context.disk_loader_factory
self.logger = context.logger
self._already_prepared = []
self.working_managers = []
self.config = config
self.settings_manager: Optional[GenericSettingsManager] = None
self.http_client = context.http_client
self.configman = CoreConfigManager()
self._action_reset: Optional[CustomSoftwareAction] = None
self._dynamic_extra_actions: Optional[Dict[CustomSoftwareAction, Callable[[dict], bool]]] = None
self.force_suggestions = force_suggestions
@property
def dynamic_extra_actions(self) -> Dict[CustomSoftwareAction, Callable[[dict], bool]]:
if self._dynamic_extra_actions is None:
self._dynamic_extra_actions = {
CustomSoftwareAction(i18n_label_key='action.backups',
i18n_status_key='action.backups.status',
i18n_description_key='action.backups.desc',
manager_method='launch_timeshift',
manager=self,
icon_path='timeshift',
requires_root=False,
refresh=False): self.is_backups_action_available
}
return self._dynamic_extra_actions
@property
def action_reset(self) -> CustomSoftwareAction:
if self._action_reset is None:
self._action_reset = CustomSoftwareAction(i18n_label_key='action.reset',
i18n_status_key='action.reset.status',
i18n_description_key='action.reset.desc',
manager_method='reset',
icon_path=resource.get_path('img/logo.svg'),
requires_root=False,
manager=self,
refresh=False)
return self._action_reset
def _is_timeshift_launcher_available(self) -> bool:
return bool(shutil.which('timeshift-launcher'))
def is_backups_action_available(self, app_config: dict) -> bool:
return bool(app_config['backup']['enabled']) and self._is_timeshift_launcher_available()
def reset_cache(self):
if self._available_cache is not None:
self._available_cache = {}
self.working_managers.clear()
def launch_timeshift(self, root_password: Optional[str], watcher: ProcessWatcher):
if self._is_timeshift_launcher_available():
try:
Popen(['timeshift-launcher'], stderr=STDOUT)
return True
except Exception:
traceback.print_exc()
watcher.show_message(title=self.i18n["error"].capitalize(),
body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')),
type_=MessageType.ERROR)
return False
else:
watcher.show_message(title=self.i18n["error"].capitalize(),
body=self.i18n['action.backups.tool_error'].format(bold('Timeshift')),
type_=MessageType.ERROR)
return False
def _can_work(self, man: SoftwareManager):
if self._available_cache is not None:
available = False
for t in man.get_managed_types():
available = self._available_cache.get(t)
if available is None:
available = man.is_enabled() and man.can_work()[0]
self._available_cache[t] = available
if available:
available = True
else:
available = man.is_enabled() and man.can_work()[0]
if available:
if man not in self.working_managers:
self.working_managers.append(man)
else:
if man in self.working_managers:
self.working_managers.remove(man)
return available
def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult):
mti = time.time()
apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1)
mtf = time.time()
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds')
res.installed.extend(apps_found.installed)
res.new.extend(apps_found.new)
def search(self, words: str, disk_loader: DiskCacheLoader = None, limit: int = -1, is_url: bool = False) -> SearchResult:
ti = time.time()
self._wait_to_be_ready()
res = SearchResult.empty()
if self.context.is_internet_available():
norm_query = sanitize_command_input(words).lower()
self.logger.info(f"Search query: {norm_query}")
if norm_query:
is_url = bool(RE_URL.match(norm_query))
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
threads = []
for man in self.managers:
if self._can_work(man):
t = Thread(target=self._search, args=(norm_query, is_url, man, disk_loader, res), daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()
if disk_loader:
disk_loader.stop_working()
disk_loader.join()
else:
raise NoInternetException()
res.update_total()
tf = time.time()
self.logger.info(f'Took {tf - ti:.8f} seconds')
return res
def _wait_to_be_ready(self):
if self.thread_prepare:
self.thread_prepare.join()
self.thread_prepare = None
def set_enabled(self, enabled: bool):
pass
def can_work(self) -> Tuple[bool, Optional[str]]:
return True, None
def _get_package_lower_name(self, pkg: SoftwarePackage):
return pkg.name.lower()
def _fill_read_installed(self, man: SoftwareManager, disk_loader: DiskCacheLoader, internet_available: bool,
output: List[SearchResult]):
mti = time.time()
man_res = man.read_installed(disk_loader=disk_loader, pkg_types=None, internet_available=internet_available,
limit=-1, only_apps=False)
mtf = time.time()
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.4f} seconds')
output.append(man_res)
def read_installed(self, disk_loader: DiskCacheLoader = None, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult:
ti = time.time()
self._wait_to_be_ready()
res = SearchResult([], None, 0)
disk_loader = None
net_available = self.context.is_internet_available()
read_threads = list()
results = list()
if not pkg_types: # any type
for man in self.managers:
if self._can_work(man):
if not disk_loader:
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
t = Thread(target=self._fill_read_installed,
args=(man, disk_loader, net_available, results),
daemon=True)
t.start()
read_threads.append(t)
else:
man_already_used = []
for t in pkg_types:
man = self.map.get(t)
if man and (man not in man_already_used) and self._can_work(man):
if not disk_loader:
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
t = Thread(target=self._fill_read_installed,
args=(man, disk_loader, net_available, results),
daemon=True)
t.start()
read_threads.append(t)
for t in read_threads:
t.join()
if disk_loader:
disk_loader.stop_working()
disk_loader.join()
for result in results:
if result.installed:
res.installed.extend(result.installed)
res.total += result.total
if res.installed:
for p in res.installed:
if p.is_update_ignored():
if p.categories is None:
p.categories = ['updates_ignored']
elif 'updates_ignored' not in p.categories:
self._add_category(p, 'updates_ignored')
res.installed.sort(key=self._get_package_lower_name)
tf = time.time()
self.logger.info(f'Took {tf - ti:.2f} seconds')
return res
def _add_category(self, pkg: SoftwarePackage, category: str):
if isinstance(pkg.categories, tuple):
pkg.categories = tuple((*pkg.categories, category))
elif isinstance(pkg.categories, list):
pkg.categories.append(category)
elif isinstance(pkg.categories, set):
pkg.categories.add(category)
def downgrade(self, app: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher) -> bool:
man = self._get_manager_for(app)
if man and app.can_be_downgraded():
mti = time.time()
res = man.downgrade(app, root_password, handler)
mtf = time.time()
self.logger.info(f'Took {mtf - mti:.2f} seconds')
return res
else:
raise Exception(f"Downgrading is not possible for {app.__class__.__name__}")
def clean_cache_for(self, app: SoftwarePackage):
man = self._get_manager_for(app)
if man:
return man.clean_cache_for(app)
def upgrade(self, requirements: GenericUpgradeRequirements, root_password: Optional[str], handler: ProcessWatcher) -> bool:
for man, man_reqs in requirements.sub_requirements.items():
res = man.upgrade(man_reqs, root_password, handler)
if not res:
return False
return True
def _fill_post_transaction_status(self, pkg: SoftwarePackage, installed: bool):
pkg.installed = installed
pkg.update = False
if pkg.latest_version:
pkg.version = pkg.latest_version
def _update_post_transaction_status(self, res: TransactionResult):
if res.success:
if res.installed:
for p in res.installed:
self._fill_post_transaction_status(p, True)
if res.removed:
for p in res.removed:
self._fill_post_transaction_status(p, False)
def uninstall(self, pkg: SoftwarePackage, root_password: Optional[str], handler: ProcessWatcher, disk_loader: DiskCacheLoader = None) -> TransactionResult:
man = self._get_manager_for(pkg)
if man:
ti = time.time()
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
self.logger.info(f"Uninstalling {pkg.name}")
try:
res = man.uninstall(pkg, root_password, handler, disk_loader)
disk_loader.stop_working()
disk_loader.join()
self._update_post_transaction_status(res)
return res
except Exception:
traceback.print_exc()
return TransactionResult(success=False, installed=[], removed=[])
finally:
tf = time.time()
self.logger.info(f'Uninstallation of {pkg} took {(tf - ti) / 60:.2f} minutes')
def install(self, app: SoftwarePackage, root_password: Optional[str], disk_loader: DiskCacheLoader, handler: ProcessWatcher) -> TransactionResult:
man = self._get_manager_for(app)
if man:
ti = time.time()
disk_loader = self.disk_loader_factory.new()
disk_loader.start()
try:
self.logger.info(f'Installing {app}')
res = man.install(app, root_password, disk_loader, handler)
disk_loader.stop_working()
disk_loader.join()
self._update_post_transaction_status(res)
return res
except Exception:
traceback.print_exc()
return TransactionResult(success=False, installed=[], removed=[])
finally:
tf = time.time()
self.logger.info(f'Installation of {app} took {(tf - ti) / 60:.2f} minutes')
def get_info(self, app: SoftwarePackage):
man = self._get_manager_for(app)
if man:
return man.get_info(app)
def get_history(self, app: SoftwarePackage) -> PackageHistory:
man = self._get_manager_for(app)
if man:
mti = time.time()
history = man.get_history(app)
mtf = time.time()
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.2f} seconds')
return history
def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
available_types = set()
for man in self.get_working_managers():
available_types.update(man.get_managed_types())
return available_types
def is_enabled(self):
return True
def _get_manager_for(self, app: SoftwarePackage) -> SoftwareManager:
man = self.map[app.__class__]
return man if man and self._can_work(man) else None
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
if pkg.supports_disk_cache():
man = self._get_manager_for(pkg)
if man:
return man.cache_to_disk(pkg, icon_bytes=icon_bytes, only_icon=only_icon)
def requires_root(self, action: SoftwareAction, app: SoftwarePackage) -> bool:
if app is None:
if self.managers:
for man in self.managers:
if self._can_work(man):
if man.requires_root(action, app):
return True
return False
else:
man = self._get_manager_for(app)
if man:
return man.requires_root(action, app)
def prepare(self, task_manager: TaskManager, root_password: Optional[str], internet_available: bool):
ti = time.time()
self.logger.info("Initializing")
taskman = task_manager if task_manager else TaskManager() # empty task manager to prevent null pointers
create_config = CreateConfigFile(taskman=taskman, configman=self.configman, i18n=self.i18n,
task_icon_path=get_path('img/logo.svg'), logger=self.logger)
create_config.start()
if self.managers:
internet_on = self.context.is_internet_available()
prepare_tasks = []
for man in self.managers:
if man not in self._already_prepared and self._can_work(man):
t = Thread(target=man.prepare, args=(taskman, root_password, internet_on), daemon=True)
t.start()
prepare_tasks.append(t)
self._already_prepared.append(man)
for t in prepare_tasks:
t.join()
create_config.join()
tf = time.time()
self.logger.info(f'Finished ({tf - ti:.2f} seconds)')
def cache_available_managers(self):
if self.managers:
for man in self.managers:
self._can_work(man)
def list_updates(self, internet_available: bool = None) -> List[PackageUpdate]:
self._wait_to_be_ready()
updates = []
if self.managers:
net_available = self.context.is_internet_available()
for man in self.managers:
if self._can_work(man):
man_updates = man.list_updates(internet_available=net_available)
if man_updates:
updates.extend(man_updates)
return updates
def list_warnings(self, internet_available: bool = None) -> List[str]:
warnings = []
int_available = self.context.is_internet_available()
if int_available:
updates_msg = check_for_update(self.logger, self.http_client, self.i18n)
if updates_msg:
warnings.append(updates_msg)
if self.managers:
for man in self.managers:
if self._can_work(man):
man_warnings = man.list_warnings(internet_available=int_available)
if man_warnings:
warnings.extend(man_warnings)
return warnings
def _fill_suggestions(self, suggestions: list, man: SoftwareManager, limit: int, filter_installed: bool):
if self._can_work(man):
mti = time.time()
man_sugs = man.list_suggestions(limit=limit, filter_installed=filter_installed)
mtf = time.time()
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.5f} seconds')
if man_sugs:
if 0 < limit < len(man_sugs):
man_sugs = man_sugs[0:limit]
suggestions.extend(man_sugs)
def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]:
if self.force_suggestions or bool(self.config['suggestions']['enabled']):
if self.managers and self.context.is_internet_available():
suggestions, threads = [], []
for man in self.managers:
t = Thread(target=self._fill_suggestions,
args=(suggestions, man, int(self.config['suggestions']['by_type']), filter_installed),
daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()
if suggestions:
suggestions.sort(key=lambda s: s.priority.value, reverse=True)
return suggestions
return []
def execute_custom_action(self, action: CustomSoftwareAction, pkg: SoftwarePackage, root_password: Optional[str], watcher: ProcessWatcher):
if action.requires_internet and not self.context.is_internet_available():
raise NoInternetException()
man = action.manager if action.manager else self._get_manager_for(pkg)
if man:
return eval(f"man.{action.manager_method}({'pkg=pkg, ' if pkg else ''}root_password=root_password, watcher=watcher)")
def is_default_enabled(self) -> bool:
return True
def launch(self, pkg: SoftwarePackage):
self._wait_to_be_ready()
man = self._get_manager_for(pkg)
if man:
self.logger.info(f'Launching {pkg}')
man.launch(pkg)
def get_screenshots(self, pkg: SoftwarePackage) -> Generator[str, None, None]:
man = self._get_manager_for(pkg)
if man:
yield from man.get_screenshots(pkg)
def get_working_managers(self):
return [m for m in self.managers if self._can_work(m)]
def get_settings(self) -> Optional[Generator[SettingsView, None, None]]:
if self.settings_manager is None:
self.settings_manager = GenericSettingsManager(managers=self.managers,
working_managers=self.working_managers,
configman=self.configman,
context=self.context)
else:
self.settings_manager.managers = self.managers
self.settings_manager.working_managers = self.working_managers
yield SettingsView(self, self.settings_manager.get_settings())
def save_settings(self, component: TabGroupComponent) -> Tuple[bool, Optional[List[str]]]:
return self.settings_manager.save_settings(component)
def _map_pkgs_by_manager(self, pkgs: List[SoftwarePackage], pkg_filters: list = None) -> Dict[SoftwareManager, List[SoftwarePackage]]:
by_manager = {}
for pkg in pkgs:
if pkg_filters and not all((1 for f in pkg_filters if f(pkg))):
continue
man = self._get_manager_for(pkg)
if man:
man_pkgs = by_manager.get(man)
if man_pkgs is None:
man_pkgs = []
by_manager[man] = man_pkgs
man_pkgs.append(pkg)
return by_manager
def get_upgrade_requirements(self, pkgs: List[SoftwarePackage], root_password: Optional[str], watcher: ProcessWatcher) -> UpgradeRequirements:
by_manager = self._map_pkgs_by_manager(pkgs)
res = GenericUpgradeRequirements([], [], [], [], {})
if by_manager:
for man, pkgs in by_manager.items():
ti = time.time()
man_reqs = man.get_upgrade_requirements(pkgs, root_password, watcher)
tf = time.time()
self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds')
if not man_reqs:
return # it means the process should be stopped
if man_reqs:
res.sub_requirements[man] = man_reqs
if man_reqs.to_install:
res.to_install.extend(man_reqs.to_install)
if man_reqs.to_remove:
res.to_remove.extend(man_reqs.to_remove)
if man_reqs.to_upgrade:
res.to_upgrade.extend(man_reqs.to_upgrade)
if man_reqs.cannot_upgrade:
res.cannot_upgrade.extend(man_reqs.cannot_upgrade)
return res
def reset(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
body = f"<p>{self.i18n['action.reset.body_1'].format(bold(self.context.app_name))}</p>" \
f"<p>{self.i18n['action.reset.body_2']}</p>"
if watcher.request_confirmation(title=self.i18n['action.reset'],
body=body,
confirmation_label=self.i18n['proceed'].capitalize(),
deny_label=self.i18n['cancel'].capitalize()):
try:
clean_app_files(managers=self.managers, logs=False)
restart_app()
except Exception:
return False
return True
def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
if self.managers:
working_managers = []
for man in self.managers:
if self._can_work(man):
working_managers.append(man)
if working_managers:
working_managers.sort(key=lambda m: m.__class__.__name__)
for man in working_managers:
for action in man.gen_custom_actions():
action.manager = man
yield action
app_config = self.configman.get_config()
for action, available in self.dynamic_extra_actions.items():
if available(app_config):
yield action
yield self.action_reset
def _fill_sizes(self, man: SoftwareManager, pkgs: List[SoftwarePackage]):
ti = time.time()
man.fill_sizes(pkgs)
tf = time.time()
self.logger.info(f'{man.__class__.__name__} took {tf - ti:.2f} seconds')
def fill_sizes(self, pkgs: List[SoftwarePackage]):
by_manager = self._map_pkgs_by_manager(pkgs, pkg_filters=[lambda p: p.size is None])
if by_manager:
threads = []
for man, man_pkgs in by_manager.items():
if man_pkgs:
t = Thread(target=self._fill_sizes, args=(man, man_pkgs), daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()
def ignore_update(self, pkg: SoftwarePackage):
manager = self._get_manager_for(pkg)
if manager:
manager.ignore_update(pkg)
if pkg.is_update_ignored():
if pkg.categories is None:
pkg.categories = ['updates_ignored']
elif 'updates_ignored' not in pkg.categories:
self._add_category(pkg, 'updates_ignored')
def revert_ignored_update(self, pkg: SoftwarePackage):
manager = self._get_manager_for(pkg)
if manager:
manager.revert_ignored_update(pkg)
if not pkg.is_update_ignored() and pkg.categories and 'updates_ignored' in pkg.categories:
if isinstance(pkg.categories, tuple):
pkg.categories = tuple(c for c in pkg.categories if c != 'updates_ignored')
else:
pkg.categories.remove('updates_ignored')

View File

@@ -1,328 +0,0 @@
import os
import re
import shutil
import time
import traceback
from io import StringIO, BytesIO
from logging import Logger
from math import floor
from pathlib import Path
from threading import Thread
from typing import Optional, Tuple
from bauh.api.abstract.download import FileDownloader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.http import HttpClient
from bauh.commons.html import bold
from bauh.commons.system import ProcessHandler, SimpleProcess
from bauh.commons.view_utils import get_human_size_str
from bauh.view.util.translation import I18n
RE_HAS_EXTENSION = re.compile(r'.+\.\w+$')
class SelfFileDownloader(FileDownloader):
def __init__(self, logger: Logger, i18n: I18n, http_client: HttpClient,
check_ssl: bool):
self._logger = logger
self._i18n = i18n
self._client = http_client
self._ssl = check_ssl
def is_multithreaded(self) -> bool:
return False
def can_work(self) -> bool:
return True
def get_supported_multithreaded_clients(self) -> Tuple[str, ...]:
return tuple()
def is_multithreaded_client_available(self, name: str) -> bool:
return False
def list_available_multithreaded_clients(self) -> Tuple[str, ...]:
return tuple()
def get_supported_clients(self) -> Tuple[str, ...]:
return tuple()
def download(self, file_url: str, watcher: Optional[ProcessWatcher], output_path: str, cwd: str,
root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True,
max_threads: int = None, known_size: int = None) -> bool:
try:
res = self._client.get(url=file_url, ignore_ssl=not self._ssl, stream=True)
except Exception:
return False
try:
content_length = int(res.headers.get("content-length", 0))
except Exception:
content_length = 0
self._logger.warning(f"Could not retrieve the content-length for file '{file_url}'")
file_name = file_url.split("/")[-1]
msg = StringIO()
msg.write(f"{substatus_prefix} " if substatus_prefix else "")
msg.write(f"{self._i18n['downloading']} {bold(file_name)}")
base_msg = msg.getvalue()
byte_stream = BytesIO()
total_downloaded = 0
known_size = content_length and content_length > 0
total_size_str = get_human_size_str(content_length) if known_size > 0 else "?"
try:
for data in res.iter_content(chunk_size=1024):
byte_stream.write(data)
total_downloaded += len(data)
perc = f"({(total_downloaded / content_length) * 100:.2f}%) " if known_size > 0 else ""
watcher.change_substatus(f"{perc}{base_msg} ({get_human_size_str(total_downloaded)} / {total_size_str})")
except Exception:
self._logger.error(f"Unexpected exception while downloading file from '{file_url}'")
traceback.print_exc()
return False
self._logger.info(f"Writing downloaded file content to disk: {output_path}")
try:
with open(output_path, "wb+") as f:
f.write(byte_stream.getvalue())
except Exception:
self._logger.error(f"Unexpected exception when saving downloaded content to disk: {output_path}")
traceback.print_exc()
return False
return True
class AdaptableFileDownloader(FileDownloader):
def __init__(self, logger: Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient,
multithread_client: str, check_ssl: bool):
self.logger = logger
self.multithread_enabled = multithread_enabled
self.i18n = i18n
self.http_client = http_client
self.supported_multithread_clients = ("aria2", "axel")
self.multithread_client = multithread_client
self.check_ssl = check_ssl
self._self_downloader = SelfFileDownloader(logger=logger,
i18n=i18n,
http_client=http_client,
check_ssl=check_ssl)
@staticmethod
def is_aria2c_available() -> bool:
return bool(shutil.which('aria2c'))
@staticmethod
def is_axel_available() -> bool:
return bool(shutil.which('axel'))
def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess:
cmd = ['aria2c', url,
'--no-conf',
'-x', '16',
'--enable-color=false',
'--stderr=true',
'--summary-interval=0',
'--disable-ipv6',
'-k', '1M',
'--allow-overwrite=true',
'-c',
'-t', '5',
'--max-file-not-found=3',
'--file-allocation=none',
'--console-log-level=error']
if threads > 1:
cmd.append('-s')
cmd.append(str(threads))
if output_path:
output_split = output_path.split('/')
cmd.append('-d')
cmd.append('/'.join(output_split[:-1]))
cmd.append('-o')
cmd.append(output_split[-1])
return SimpleProcess(cmd=cmd, root_password=root_password, cwd=cwd)
def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str], threads: int) -> SimpleProcess:
cmd = ['axel', url, '-n', str(threads), '-4', '-c', '-T', '5']
if not self.check_ssl:
cmd.append('-k')
if output_path:
cmd.append(f'--output={output_path}')
return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password)
def _rm_bad_file(self, file_name: str, output_path: str, cwd, handler: ProcessHandler, root_password: Optional[str]):
to_delete = output_path if output_path else f'{cwd}/{file_name}'
if to_delete and os.path.exists(to_delete):
self.logger.info(f'Removing downloaded file {to_delete}')
success, _ = handler.handle_simple(SimpleProcess(['rm', '-rf', to_delete], root_password=root_password))
return success
def _concat_file_size(self, file_url: str, base_substatus: StringIO, watcher: ProcessWatcher):
watcher.change_substatus(f'{base_substatus.getvalue()} ( ? Mb )')
try:
size = self.http_client.get_content_length(file_url)
if size:
base_substatus.write(f' ( {size} )')
watcher.change_substatus(base_substatus.getvalue())
except Exception:
pass
def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> int:
if max_threads and max_threads > 0:
threads = max_threads
elif known_size:
threads = 16 if known_size >= 16000000 else floor(known_size / 1000000)
if threads <= 0:
threads = 1
else:
threads = 16
return threads
def _download_with_threads(self, client: str, file_url: str, output_path: str, cwd: str,
max_threads: int, known_size: int, display_file_size: bool, handler: ProcessHandler,
root_password: Optional[str] = None, substatus_prefix: Optional[str] = None) \
-> Tuple[float, bool]:
threads = self._get_appropriate_threads_number(max_threads, known_size)
if client == 'aria2':
start_time = time.time()
process = self._get_aria2c_process(file_url, output_path, cwd, root_password, threads)
downloader = 'aria2'
else:
start_time = time.time()
process = self._get_axel_process(file_url, output_path, cwd, root_password, threads)
downloader = 'axel'
name = file_url.split('/')[-1]
if output_path and not RE_HAS_EXTENSION.match(name) and RE_HAS_EXTENSION.match(output_path):
name = output_path.split('/')[-1]
if handler.watcher:
msg = StringIO()
msg.write(f'{substatus_prefix} ' if substatus_prefix else '')
msg.write(f"{bold('[{}]'.format(downloader))} {self.i18n['downloading']} {bold(name)}")
if display_file_size:
if known_size:
msg.write(f' ( {get_human_size_str(known_size)} )')
handler.watcher.change_substatus(msg.getvalue())
else:
Thread(target=self._concat_file_size, args=(file_url, msg, handler.watcher), daemon=True).start()
else:
msg.write(' ( ? Mb )')
handler.watcher.change_substatus(msg.getvalue())
success, _ = handler.handle_simple(process)
return start_time, success
def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: Optional[str] = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool:
self.logger.info(f'Downloading {file_url}')
handler = ProcessHandler(watcher)
file_name = file_url.split('/')[-1]
final_cwd = cwd if cwd else '.'
success = False
start_time = time.time()
try:
if output_path:
if os.path.exists(output_path):
self.logger.info(f'Removing old file found before downloading: {output_path}')
os.remove(output_path)
self.logger.info(f'Old file {output_path} removed')
else:
output_dir = os.path.dirname(output_path)
try:
Path(output_dir).mkdir(exist_ok=True, parents=True)
except OSError:
self.logger.error(f"Could not make download directory '{output_dir}'")
watcher.print(self.i18n['error.mkdir'].format(dir=output_dir))
return False
threaded_client = self.get_available_multithreaded_tool()
if threaded_client:
start_time, success = self._download_with_threads(client=threaded_client, file_url=file_url,
output_path=output_path,
cwd=final_cwd, max_threads=max_threads,
known_size=known_size, handler=handler,
display_file_size=display_file_size,
root_password=root_password)
else:
start_time = time.time()
success = self._self_downloader.download(file_url=file_url, watcher=watcher, output_path=output_path,
cwd=cwd, root_password=root_password,
substatus_prefix=substatus_prefix,
display_file_size=display_file_size, max_threads=max_threads,
known_size=known_size)
except Exception:
traceback.print_exc()
self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password)
final_time = time.time()
self.logger.info(f'{file_name} download took {(final_time - start_time) / 60:.4f} minutes')
if not success:
self.logger.error(f"Could not download '{file_name}'")
self._rm_bad_file(file_name, output_path, final_cwd, handler, root_password)
return success
def is_multithreaded(self) -> bool:
return bool(self.get_available_multithreaded_tool())
def get_available_multithreaded_tool(self) -> str:
if self.multithread_enabled:
if self.multithread_client is None or self.multithread_client not in self.supported_multithread_clients:
for client in self.supported_multithread_clients:
if self.is_multithreaded_client_available(client):
return client
else:
possible_clients = {*self.supported_multithread_clients}
if self.is_multithreaded_client_available(self.multithread_client):
return self.multithread_client
else:
possible_clients.remove(self.multithread_client)
for client in possible_clients:
if self.is_multithreaded_client_available(client):
return client
def can_work(self) -> bool:
return True
def get_supported_multithreaded_clients(self) -> Tuple[str, ...]:
return self.supported_multithread_clients
def is_multithreaded_client_available(self, name: str) -> bool:
if name == 'aria2':
return self.is_aria2c_available()
elif name == 'axel':
return self.is_axel_available()
else:
return False
def list_available_multithreaded_clients(self) -> Tuple[str, ...]:
return tuple(c for c in self.supported_multithread_clients if self.is_multithreaded_client_available(c))
def get_supported_clients(self) -> Tuple[str, ...]:
return "self", "aria2", "axel"

View File

@@ -1,32 +0,0 @@
import os
from typing import Optional, Dict
from bauh import __app_name__
def read_suggestions_mapping() -> Optional[Dict[str, str]]:
file_path = f'/etc/{__app_name__}/suggestions.conf'
if os.path.isfile(file_path):
try:
with open(file_path) as f:
file_content = f.read()
except FileNotFoundError:
return
if not file_content:
return
mapping = {}
for line in file_content.split('\n'):
line_strip = line.strip()
if not line_strip.startswith('#'):
gem_file = line_strip.split('=')
if len(gem_file) == 2:
gem_name, file_url = gem_file[0].strip(), gem_file[1].strip()
if gem_name and file_url:
mapping[gem_name] = file_url
return mapping if mapping else None

View File

@@ -1,453 +0,0 @@
import datetime
import operator
import time
from functools import reduce
from typing import Tuple, Optional
from PyQt5.QtCore import QSize, Qt, QThread, pyqtSignal, QCoreApplication, QMutex
from PyQt5.QtGui import QIcon, QCursor, QCloseEvent, QShowEvent
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy, QTableWidget, QHeaderView, QPushButton, \
QProgressBar, QPlainTextEdit, QToolButton, QHBoxLayout
from bauh import __app_name__
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager, SoftwareAction
from bauh.api.abstract.handler import TaskManager
from bauh.api import user
from bauh.view.qt.components import new_spacer, QCustomToolbar
from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry
from bauh.view.qt.root import RootDialog
from bauh.view.qt.thread import AnimateProgress
from bauh.view.util.translation import I18n
class Prepare(QThread, TaskManager):
signal_register = pyqtSignal(str, str, object)
signal_update = pyqtSignal(str, float, str)
signal_finished = pyqtSignal(str)
signal_started = pyqtSignal(int)
signal_ask_password = pyqtSignal()
signal_output = pyqtSignal(str, str)
def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n):
super(Prepare, self).__init__()
self.manager = manager
self.i18n = i18n
self.context = context
self.waiting_password = False
self.password_response = None
self._tasks_added = set()
self._tasks_finished = set()
self._add_lock = QMutex()
self._finish_lock = QMutex()
def ask_password(self) -> Tuple[bool, Optional[str]]:
self.waiting_password = True
self.signal_ask_password.emit()
while self.waiting_password:
self.msleep(100) # waiting for user input
return self.password_response
def set_password_reply(self, valid: bool, password: str):
self.password_response = valid, password
self.waiting_password = False
def run(self):
root_pwd = None
if not user.is_root() and self.manager.requires_root(SoftwareAction.PREPARE, None):
ok, root_pwd = self.ask_password()
if not ok:
QCoreApplication.exit(1)
self.manager.prepare(self, root_pwd, None)
self.signal_started.emit(len(self._tasks_added))
def update_progress(self, task_id: str, progress: float, substatus: str):
self._add_lock.lock()
if task_id in self._tasks_added:
self.signal_update.emit(task_id, progress, substatus)
self._add_lock.unlock()
def update_output(self, task_id: str, output: str):
self._add_lock.lock()
if task_id in self._tasks_added:
self.signal_output.emit(task_id, output)
self._add_lock.unlock()
def register_task(self, id_: str, label: str, icon_path: str):
self._add_lock.lock()
if id_ not in self._tasks_added:
self._tasks_added.add(id_)
self.signal_register.emit(id_, label, icon_path)
self._add_lock.unlock()
def finish_task(self, task_id: str):
self._add_lock.lock()
task_registered = task_id in self._tasks_added
self._add_lock.unlock()
if not task_registered:
return
self._finish_lock.lock()
if task_id not in self._tasks_finished:
self._tasks_finished.add(task_id)
self.signal_finished.emit(task_id)
self._finish_lock.unlock()
class CheckFinished(QThread):
signal_finished = pyqtSignal()
def __init__(self):
super(CheckFinished, self).__init__()
self.total = 0
self.finished = 0
def run(self):
while True:
if self.finished == self.total:
break
self.msleep(5)
self.signal_finished.emit()
def update(self, finished: int):
if finished is not None:
self.finished = finished
class EnableSkip(QThread):
signal_timeout = pyqtSignal()
def run(self):
ti = datetime.datetime.now()
while True:
if datetime.datetime.now() >= ti + datetime.timedelta(seconds=10):
self.signal_timeout.emit()
break
self.msleep(100)
class PreparePanel(QWidget, TaskManager):
signal_status = pyqtSignal(int)
signal_password_response = pyqtSignal(bool, str)
def __init__(self, context: ApplicationContext, manager: SoftwareManager,
i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False):
super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.i18n = i18n
self.context = context
self.app_config = app_config
self.manage_window = manage_window
self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower()))
self.setLayout(QVBoxLayout())
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.manager = manager
self.tasks = {}
self.output = {}
self.added_tasks = 0
self.ftasks = 0
self.started_at = None
self.self_close = False
self.force_suggestions = force_suggestions
self.prepare_thread = Prepare(self.context, manager, self.i18n)
self.prepare_thread.signal_register.connect(self.register_task)
self.prepare_thread.signal_update.connect(self.update_progress)
self.prepare_thread.signal_finished.connect(self.finish_task)
self.prepare_thread.signal_started.connect(self.start)
self.prepare_thread.signal_ask_password.connect(self.ask_root_password)
self.prepare_thread.signal_output.connect(self.update_output)
self.signal_password_response.connect(self.prepare_thread.set_password_reply)
self.check_thread = CheckFinished()
self.signal_status.connect(self.check_thread.update)
self.check_thread.signal_finished.connect(self.finish)
self.skip_thread = EnableSkip()
self.skip_thread.signal_timeout.connect(self._enable_skip_button)
self.progress_thread = AnimateProgress()
self.progress_thread.signal_change.connect(self._change_progress)
self.label_top = QLabel()
self.label_top.setCursor(QCursor(Qt.WaitCursor))
self.label_top.setText("{}...".format(self.i18n['prepare_panel.title.start'].capitalize()))
self.label_top.setObjectName('prepare_status')
self.label_top.setAlignment(Qt.AlignHCenter)
self.layout().addWidget(self.label_top)
self.layout().addWidget(QLabel())
self.table = QTableWidget()
self.table.setObjectName('tasks')
self.table.setCursor(QCursor(Qt.WaitCursor))
self.table.setFocusPolicy(Qt.NoFocus)
self.table.setShowGrid(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setVisible(False)
self.table.horizontalHeader().setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(['' for _ in range(4)])
self.table.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
self.table.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
self.layout().addWidget(self.table)
self.textarea_details = QPlainTextEdit(self)
self.textarea_details.setObjectName('task_details')
self.textarea_details.setProperty('console', 'true')
self.textarea_details.resize(self.table.size())
self.layout().addWidget(self.textarea_details)
self.textarea_details.setVisible(False)
self.textarea_details.setReadOnly(True)
self.textarea_details.setMaximumHeight(100)
self.current_output_task = None
self.bottom_widget = QWidget()
self.bottom_widget.setLayout(QHBoxLayout())
self.bottom_widget.layout().addStretch()
bt_hide_details = QPushButton(self.i18n['prepare.bt_hide_details'])
bt_hide_details.setObjectName('bt_hide_details')
bt_hide_details.clicked.connect(self.hide_output)
bt_hide_details.setCursor(QCursor(Qt.PointingHandCursor))
self.bottom_widget.layout().addWidget(bt_hide_details)
self.bottom_widget.layout().addStretch()
self.layout().addWidget(self.bottom_widget)
self.bottom_widget.setVisible(False)
self.bt_bar = QCustomToolbar(policy_height=QSizePolicy.Fixed)
self.bt_close = QPushButton(self.i18n['close'].capitalize())
self.bt_close.setObjectName('bt_cancel')
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
self.bt_close.clicked.connect(self.close)
self.bt_close.setVisible(False)
self.bt_bar.add_widget(self.bt_close)
self.bt_bar.add_widget(new_spacer())
self.progress_bar = QProgressBar()
self.progress_bar.setObjectName('prepare_progress')
self.progress_bar.setTextVisible(False)
self.progress_bar.setVisible(False)
self.progress_bar.setCursor(QCursor(Qt.WaitCursor))
self.bt_bar.add_widget(self.progress_bar)
self.bt_bar.add_widget(new_spacer())
self.bt_skip = QPushButton(self.i18n['prepare_panel.bt_skip.label'].capitalize())
self.bt_skip.clicked.connect(self.finish)
self.bt_skip.setEnabled(False)
self.bt_skip.setCursor(QCursor(Qt.WaitCursor))
self.bt_bar.add_widget(self.bt_skip)
self.layout().addWidget(self.bt_bar)
centralize(self)
def hide_output(self):
self.current_output_task = None
self.textarea_details.setVisible(False)
self.textarea_details.clear()
self.bottom_widget.setVisible(False)
self._resize_columns()
self.setFocus(Qt.NoFocusReason)
if not self.bt_bar.isVisible():
self.bt_bar.setVisible(True)
def ask_root_password(self):
valid, root_pwd = RootDialog.ask_password(self.context, self.i18n)
self.signal_password_response.emit(valid, root_pwd)
def _enable_skip_button(self):
self.bt_skip.setEnabled(True)
self.bt_skip.setCursor(QCursor(Qt.PointingHandCursor))
def _change_progress(self, value: int):
self.progress_bar.setValue(value)
def get_table_width(self) -> int:
return reduce(operator.add, [self.table.columnWidth(i) for i in range(self.table.columnCount())])
def _resize_columns(self):
header_horizontal = self.table.horizontalHeader()
for i in range(self.table.columnCount()):
header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents)
self.resize(int(self.get_table_width() * 1.05), self.sizeHint().height())
def showEvent(self, event: Optional[QShowEvent]) -> None:
super().showEvent(event)
self.prepare_thread.start()
screen_size = get_current_screen_geometry()
self.setMinimumWidth(int(screen_size.width() * 0.25))
self.setMinimumHeight(int(screen_size.height() * 0.35))
self.setMaximumHeight(int(screen_size.height() * 0.95))
centralize(self)
def start(self, tasks: int):
self.started_at = time.time()
self.check_thread.total = tasks
self.check_thread.start()
self.skip_thread.start()
self.progress_thread.start()
self.bt_close.setVisible(True)
self.progress_bar.setVisible(True)
def closeEvent(self, ev: QCloseEvent):
if not self.self_close:
QCoreApplication.exit()
def register_task(self, id_: str, label: str, icon_path: str):
self.added_tasks += 1
self.table.setRowCount(self.added_tasks)
task_row = self.added_tasks - 1
icon_widget = QWidget()
icon_widget.setProperty('container', 'true')
icon_widget.setLayout(QHBoxLayout())
icon_widget.layout().setContentsMargins(10, 0, 10, 0)
bt_icon = QToolButton()
bt_icon.setObjectName('bt_task')
bt_icon.setCursor(QCursor(Qt.WaitCursor))
bt_icon.setEnabled(False)
bt_icon.setToolTip(self.i18n['prepare.bt_icon.no_output'])
if icon_path:
bt_icon.setIcon(QIcon(icon_path))
def _show_output():
lines = self.output[id_]
if lines:
self.current_output_task = id_
self.textarea_details.clear()
self.textarea_details.setVisible(True)
for l in lines:
self.textarea_details.appendPlainText(l)
self.bottom_widget.setVisible(True)
self.setFocus(Qt.NoFocusReason)
if self.bt_bar.isVisible():
self.bt_bar.setVisible(False)
bt_icon.clicked.connect(_show_output)
icon_widget.layout().addWidget(bt_icon)
self.table.setCellWidget(task_row, 0, icon_widget)
lb_status = QLabel(label)
lb_status.setObjectName('task_status')
lb_status.setProperty('status', 'running')
lb_status.setCursor(Qt.WaitCursor)
lb_status.setMinimumWidth(50)
lb_status.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
lb_status_col = 1
self.table.setCellWidget(task_row, lb_status_col, lb_status)
lb_progress = QLabel('{0:.2f}'.format(0) + '%')
lb_progress.setObjectName('task_progress')
lb_progress.setProperty('status', 'running')
lb_progress.setCursor(Qt.WaitCursor)
lb_progress.setContentsMargins(10, 0, 10, 0)
lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
lb_progress_col = 2
self.table.setCellWidget(task_row, lb_progress_col, lb_progress)
lb_sub = QLabel()
lb_sub.setObjectName('task_substatus')
lb_sub.setCursor(Qt.WaitCursor)
lb_sub.setContentsMargins(10, 0, 10, 0)
lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
lb_sub.setMinimumWidth(50)
self.table.setCellWidget(task_row, 3, lb_sub)
self.tasks[id_] = {'bt_icon': bt_icon,
'lb_status': lb_status,
'lb_status_col': lb_status_col,
'lb_prog': lb_progress,
'lb_prog_col': lb_progress_col,
'progress': 0,
'lb_sub': lb_sub,
'finished': False,
'row': task_row}
def update_progress(self, task_id: str, progress: float, substatus: str):
task = self.tasks[task_id]
if progress != task['progress']:
task['progress'] = progress
task['lb_prog'].setText('{0:.2f}'.format(progress) + '%')
if substatus:
task['lb_sub'].setText('({})'.format(substatus))
else:
task['lb_sub'].setText('')
self._resize_columns()
def update_output(self, task_id: str, output: str):
full_output = self.output.get(task_id)
if full_output is None:
full_output = []
self.output[task_id] = full_output
task = self.tasks[task_id]
task['bt_icon'].setEnabled(True)
task['bt_icon'].setCursor(QCursor(Qt.PointingHandCursor))
task['bt_icon'].setToolTip(self.i18n['prepare.bt_icon.output'])
full_output.append(output)
if self.current_output_task == task_id:
self.textarea_details.appendPlainText(output)
def finish_task(self, task_id: str):
task = self.tasks[task_id]
for key in ('lb_prog', 'lb_status', 'lb_sub'):
label = task[key]
label.setProperty('status', 'done')
label.style().unpolish(label)
label.style().polish(label)
label.update()
task['finished'] = True
self._resize_columns()
self.ftasks += 1
self.signal_status.emit(self.ftasks)
def finish(self):
now = time.time()
self.context.logger.info("{0} tasks finished in {1:.9f} seconds".format(self.ftasks, (now - self.started_at)))
if self.isVisible():
self.manage_window.show()
if self.force_suggestions:
self.manage_window.begin_load_suggestions(filter_installed=True)
elif self.app_config['boot']['load_apps']:
self.manage_window.begin_refresh_packages()
else:
self.manage_window.load_without_packages()
self.self_close = True
self.close()

View File

@@ -1,149 +0,0 @@
import gc
from io import StringIO
from typing import Optional
from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal
from PyQt5.QtGui import QCursor, QShowEvent
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QPushButton, QHBoxLayout, QApplication
from bauh import __app_name__
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.view import MessageType
from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.qt import dialog
from bauh.view.qt.components import to_widget, new_spacer
from bauh.view.qt.dialog import ConfirmationDialog
from bauh.view.qt.qt_utils import centralize
from bauh.view.util import util
from bauh.view.util.translation import I18n
class ReloadManagePanel(QThread):
signal_finished = pyqtSignal()
def __init__(self, manager: SoftwareManager):
super(ReloadManagePanel, self).__init__()
self.manager = manager
def run(self):
if isinstance(self.manager, GenericSoftwareManager):
self.manager.reset_cache()
self.manager.prepare(task_manager=None, root_password=None, internet_available=None)
self.signal_finished.emit()
class SettingsWindow(QWidget):
def __init__(self, manager: SoftwareManager, i18n: I18n, window: QWidget, parent: Optional[QWidget] = None):
super(SettingsWindow, self).__init__(parent=parent, flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.setWindowTitle(f"{i18n['settings'].capitalize()} ({__app_name__})")
self.setLayout(QVBoxLayout())
self.manager = manager
self.i18n = i18n
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.window = window
self.settings_model = tuple(v for v in self.manager.get_settings())[0].component
self.tab_group = to_widget(self.settings_model, i18n)
self.tab_group.setObjectName('settings')
self.layout().addWidget(self.tab_group)
lower_container = QWidget()
lower_container.setObjectName('lower_container')
lower_container.setProperty('container', 'true')
lower_container.setLayout(QHBoxLayout())
lower_container.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
self.bt_close = QPushButton()
self.bt_close.setObjectName('cancel')
self.bt_close.setAutoDefault(True)
self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
self.bt_close.setText(self.i18n['close'].capitalize())
self.bt_close.clicked.connect(lambda: self.close())
lower_container.layout().addWidget(self.bt_close)
lower_container.layout().addWidget(new_spacer())
self.bt_change = QPushButton()
self.bt_change.setAutoDefault(True)
self.bt_change.setObjectName('ok')
self.bt_change.setCursor(QCursor(Qt.PointingHandCursor))
self.bt_change.setText(self.i18n['change'].capitalize())
self.bt_change.clicked.connect(self._save_settings)
lower_container.layout().addWidget(self.bt_change)
self.layout().addWidget(lower_container)
self.thread_reload_panel = ReloadManagePanel(manager=manager)
self.thread_reload_panel.signal_finished.connect(self._reload_manage_panel)
centralize(self)
def showEvent(self, event: Optional[QShowEvent]):
super(SettingsWindow, self).showEvent(event)
self.setMinimumWidth(int(self.sizeHint().width()))
centralize(self)
def closeEvent(self, event):
if self.window and self.window.settings_window == self:
self.deleteLater()
self.window.settings_window = None
elif not self.window:
QCoreApplication.exit()
gc.collect()
def handle_display(self):
if self.isMinimized():
self.setWindowState(Qt.WindowNoState)
elif self.isHidden():
self.show()
else:
self.setWindowState(self.windowState() and Qt.WindowMinimized or Qt.WindowActive)
def _save_settings(self):
self.tab_group.setEnabled(False)
self.bt_change.setEnabled(False)
self.bt_close.setEnabled(False)
success, warnings = self.manager.save_settings(self.settings_model)
if success:
if not self.window:
ConfirmationDialog(title=self.i18n['success'].capitalize(),
body=f"<p>{self.i18n['settings.changed.success.warning']}</p>",
i18n=self.i18n,
confirmation_label=self.i18n['ok'],
confirmation_icon=False,
deny_button=False).ask()
QCoreApplication.exit()
elif ConfirmationDialog(title=self.i18n['warning'].capitalize(),
body=f"<p>{self.i18n['settings.changed.success.warning']}</p>"
f"<p>{self.i18n['settings.changed.success.reboot']}</p>",
i18n=self.i18n).ask():
self.close()
util.restart_app()
else:
self.thread_reload_panel.start()
QApplication.setOverrideCursor(Qt.WaitCursor)
else:
msg = StringIO()
msg.write(f"<p>{self.i18n['settings.error']}</p>")
for w in warnings:
msg.write(f'<p style="font-weight: bold">* {w}</p><br/>')
dialog.show_message(title=self.i18n['warning'].capitalize(), body=msg.getvalue(), type_=MessageType.WARNING)
self.tab_group.setEnabled(True)
self.bt_change.setEnabled(True)
self.bt_close.setEnabled(True)
def _reload_manage_panel(self):
if self.window and self.window.isVisible():
self.window.reload()
QApplication.restoreOverrideCursor()
self.close()

View File

@@ -1,275 +0,0 @@
import json
import logging
import os
import shutil
import sys
import traceback
from io import StringIO
from subprocess import Popen
from threading import Lock, Thread
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal, QCoreApplication, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu
from bauh import __app_name__, ROOT_DIR
from bauh.api.abstract.model import PackageUpdate
from bauh.api.http import HttpClient
from bauh.commons import system
from bauh.context import generate_i18n
from bauh.view.core.tray_client import TRAY_CHECK_FILE
from bauh.view.core.update import check_for_update
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.qt_utils import load_resource_icon
from bauh.view.util import util, resource
from bauh.view.util.translation import I18n
CLI_NAME = f'{__app_name__}-cli'
def get_cli_path() -> str:
if os.getenv('APPIMAGE'):
return f"{os.environ['APPRUN_STARTUP_EXEC_PATH']} {os.environ['APPDIR']}usr/bin/{CLI_NAME}"
venv = os.getenv('VIRTUAL_ENV')
if venv:
cli_path = f'{venv}/bin/{CLI_NAME}'
if os.path.exists(cli_path):
return cli_path
elif not sys.executable.startswith('/usr'):
cli_path = f'{sys.prefix}/bin/{CLI_NAME}'
if os.path.exists(cli_path):
return cli_path
else:
return shutil.which(CLI_NAME)
def list_updates(logger: logging.Logger) -> List[PackageUpdate]:
cli_path = get_cli_path()
if cli_path:
exitcode, output = system.execute(f'{cli_path} updates -f json')
if exitcode != 0:
output_log = output.replace('\n', ' ') if output else ' '
logger.warning(f'Command "{CLI_NAME} updates" returned an unexpected exitcode ({exitcode}). Output: {output_log}')
elif output:
return [PackageUpdate(pkg_id=o['id'], name=o['name'], version=o['version'], pkg_type=o['type']) for o in json.loads(output)]
else:
logger.info("No updates found")
else:
logger.warning(f'"{CLI_NAME}" seems not to be installed')
return []
class UpdateCheck(QThread):
signal = pyqtSignal(list)
def __init__(self, check_interval: int, lock: Lock, check_file: bool, logger: logging.Logger, parent=None):
super(UpdateCheck, self).__init__(parent)
self.check_interval = check_interval
self.lock = lock
self.check_file = check_file
self.logger = logger
def _notify_updates(self):
with self.lock:
updates = list_updates(self.logger)
if updates is not None:
self.signal.emit(updates)
self.sleep(int(self.check_interval * 60))
def run(self):
while True:
if self.check_file:
if os.path.exists(TRAY_CHECK_FILE):
self._notify_updates()
try:
os.remove(TRAY_CHECK_FILE)
except Exception:
traceback.print_exc()
else:
self.sleep(self.check_interval)
else:
self._notify_updates()
class AppUpdateCheck(QThread):
def __init__(self, http_client: HttpClient, logger: logging.Logger, i18n: I18n, interval: int = 300):
super(AppUpdateCheck, self).__init__()
self.interval = interval
self.http_client = http_client
self.logger = logger
self.i18n = i18n
def run(self):
while True:
update_msg = check_for_update(http_client=self.http_client, logger=self.logger, i18n=self.i18n, tray=True)
if update_msg:
util.notify_user(msg=update_msg)
self.sleep(self.interval)
class TrayIcon(QSystemTrayIcon):
def __init__(self, config: dict, screen_size: QSize, logger: logging.Logger, manage_process: Popen = None, settings_process: Popen = None):
super(TrayIcon, self).__init__()
self.app_config = config
self.i18n = generate_i18n(config, resource.get_path('locale/tray'))
self.screen_size = screen_size
self.manage_process = manage_process
self.settings_process = settings_process
self.logger = logger
self.http_client = HttpClient(logger=logger)
if config['ui']['tray']['default_icon']:
self.icon_default = QIcon(config['ui']['tray']['default_icon'])
else:
self.icon_default = QIcon.fromTheme(f'{__app_name__}_tray_default')
if self.icon_default.isNull():
self.icon_default = QIcon.fromTheme('bauh_tray_default')
if self.icon_default.isNull():
self.icon_default = load_resource_icon('img/logo.svg', 24)
if config['ui']['tray']['updates_icon']:
self.icon_updates = QIcon(config['ui']['tray']['updates_icon'])
else:
self.icon_updates = QIcon.fromTheme(f'{__app_name__}_tray_updates')
if self.icon_updates.isNull():
self.icon_updates = QIcon.fromTheme('bauh_tray_updates')
if self.icon_updates.isNull():
self.icon_updates = load_resource_icon('img/logo_update.svg', 24)
self.setIcon(self.icon_default)
self.menu = QMenu()
self.action_manage = self.menu.addAction(self.i18n['tray.action.manage'])
self.action_manage.triggered.connect(self.show_manage_window)
self.action_settings = self.menu.addAction(self.i18n['tray.settings'].capitalize())
self.action_settings.triggered.connect(self.show_settings_window)
self.action_about = self.menu.addAction(self.i18n['tray.action.about'])
self.action_about.triggered.connect(self.show_about)
self.action_exit = self.menu.addAction(self.i18n['tray.action.exit'])
self.action_exit.triggered.connect(lambda: QCoreApplication.exit())
self.setContextMenu(self.menu)
self.manage_window = None
self.dialog_about = None
self.settings_window = None
self.check_lock = Lock()
self.check_thread = UpdateCheck(check_interval=int(config['updates']['check_interval']), check_file=False, lock=self.check_lock, logger=logger)
self.check_thread.signal.connect(self.notify_updates)
self.check_thread.start()
self.recheck_thread = UpdateCheck(check_interval=5, check_file=True, lock=self.check_lock, logger=logger)
self.recheck_thread.signal.connect(self.notify_updates)
self.recheck_thread.start()
self.update_thread = AppUpdateCheck(http_client=self.http_client, logger=self.logger, i18n=self.i18n)
self.update_thread.start()
self.last_updates = set()
self.update_notification = bool(config['system']['notifications'])
self.lock_notify = Lock()
self.activated.connect(self.handle_click)
self.set_default_tooltip()
def set_default_tooltip(self):
self.setToolTip(f"{self.i18n['tray.action.manage']} ({__app_name__})".lower())
def handle_click(self, reason):
if reason == self.Trigger:
self.show_manage_window()
def verify_updates(self, notify_user: bool = True):
Thread(target=self._verify_updates, args=(notify_user,), daemon=True).start()
def _verify_updates(self, notify_user: bool):
self.notify_updates(self.manager.list_updates(), notify_user=notify_user)
def notify_updates(self, updates: List[PackageUpdate], notify_user: bool = True):
self.lock_notify.acquire()
try:
if len(updates) > 0:
self.logger.info(f"{len(updates)} updates available")
update_keys = {f'{up.type}:{up.id}:{up.version}' for up in updates}
new_icon = self.icon_updates
if update_keys.difference(self.last_updates):
self.last_updates = update_keys
n_updates = len(updates)
ups_by_type = {}
for key in update_keys:
ptype = key.split(':')[0]
count = ups_by_type.get(ptype)
count = 1 if count is None else count + 1
ups_by_type[ptype] = count
msg = StringIO()
msg.write(self.i18n[f"notification.update{'' if n_updates == 1 else 's'}"].format(n_updates))
if len(ups_by_type) > 1:
for ptype in sorted(ups_by_type):
msg.write(f'\n * {ptype} ({ups_by_type[ptype]})')
msg.seek(0)
msg = msg.read()
self.setToolTip(msg)
if self.update_notification and notify_user:
util.notify_user(msg=msg)
else:
self.last_updates.clear()
new_icon = self.icon_default
self.set_default_tooltip()
if self.icon().cacheKey() != new_icon.cacheKey(): # changes the icon if needed
self.setIcon(new_icon)
finally:
self.lock_notify.release()
def show_manage_window(self):
if self.manage_process is None:
self.manage_process = Popen([sys.executable, f'{ROOT_DIR}/app.py'])
elif self.manage_process.poll() is not None: # it means it has finished
self.manage_process = None
self.show_manage_window()
def show_settings_window(self):
if self.settings_process is None:
self.settings_process = Popen([sys.executable, f'{ROOT_DIR}/app.py', '--settings'])
elif self.settings_process.poll() is not None: # it means it has finished
self.settings_process = None
self.show_settings_window()
def show_about(self):
if self.dialog_about is None:
self.dialog_about = AboutDialog(self.app_config)
if self.dialog_about.isHidden():
self.dialog_about.show()

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +0,0 @@
import datetime
from threading import Lock
from typing import Optional
from bauh.api.abstract.cache import MemoryCache, MemoryCacheFactory
class DefaultMemoryCache(MemoryCache):
"""
A synchronized cache implementation
"""
def __init__(self, expiration_time: int):
super(DefaultMemoryCache, self).__init__()
self.expiration_time = expiration_time
self._cache = {}
self.lock = Lock()
def is_enabled(self):
return self.expiration_time < 0 or self.expiration_time > 0
def add(self, key: str, val: object):
if key and self.is_enabled():
self.lock.acquire()
self._add(key, val)
self.lock.release()
def _add(self, key: str, val: object):
if key:
self._cache[key] = {'val': val, 'expires_at': datetime.datetime.now(UTC) + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
def add_non_existing(self, key: str, val: object):
if key and self. is_enabled():
self.lock.acquire()
cur_val = self.get(key, lock=False)
if cur_val is None:
self._add(key, val)
self.lock.release()
def get(self, key: str, lock: bool = True):
if key and self.is_enabled():
val = self._cache.get(key)
if val:
expiration = val.get('expires_at')
if expiration and expiration <= datetime.datetime.now(UTC):
if lock:
self.lock.acquire()
del self._cache[key]
if lock:
self.lock.release()
return None
return val['val']
def delete(self, key):
if key and self.is_enabled():
if key in self._cache:
self.lock.acquire()
del self._cache[key]
self.lock.release()
def keys(self):
return set(self._cache.keys()) if self.is_enabled() else set()
def clean_expired(self):
if self.is_enabled():
for key in self.keys():
self.get(key)
class DefaultMemoryCacheFactory(MemoryCacheFactory):
def __init__(self, expiration_time: int):
"""
:param expiration_time: default expiration time for all instantiated caches
"""
super(DefaultMemoryCacheFactory, self).__init__()
self.expiration_time = expiration_time
def new(self, expiration: Optional[int] = None) -> MemoryCache:
return DefaultMemoryCache(expiration if expiration is not None else self.expiration_time)
UTC = datetime.timezone.utc

View File

@@ -1,115 +0,0 @@
import json
import logging
import os
import time
from threading import Thread, Lock
from typing import Type, Dict, Any, Optional
import yaml
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
from bauh.api.abstract.model import SoftwarePackage
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
def __init__(self, cache_map: Dict[Type[SoftwarePackage], MemoryCache], logger: logging.Logger):
super(AsyncDiskCacheLoader, self).__init__(daemon=True)
self.pkgs = []
self._work = True
self.lock = Lock()
self.cache_map = cache_map
self.logger = logger
self.processed = 0
self._working = False
def fill(self, pkg: SoftwarePackage, sync: bool = False):
"""
Adds a package which data must be read from the disk to a queue (if not sync)
:param pkg:
:param sync:
:return:
"""
if pkg and pkg.supports_disk_cache():
if sync or not self._working:
self._fill_cached_data(pkg)
else:
self.pkgs.append(pkg)
def read(self, pkg: SoftwarePackage) -> Optional[Dict[str, Any]]:
if pkg and pkg.supports_disk_cache():
data_path = pkg.get_disk_data_path()
if data_path and os.path.isfile(data_path):
ext = data_path.split('.')[-1]
try:
with open(data_path) as f:
file_content = f.read()
except FileNotFoundError:
return
if file_content:
if ext == 'json':
cached_data = json.loads(file_content)
elif ext in {'yml', 'yaml'}:
cached_data = yaml.load(file_content)
else:
raise Exception(f'The cached data file {data_path} has an unsupported format')
if cached_data:
return cached_data
else:
self.logger.warning(f"No cached content in file {data_path}")
def stop_working(self):
self._work = False
def run(self):
self._working = True
last = 0
while True:
time.sleep(0.00001)
if len(self.pkgs) > self.processed:
pkg = self.pkgs[last]
self._fill_cached_data(pkg)
self.processed += 1
last += 1
elif not self._work:
break
self._working = False
def _fill_cached_data(self, pkg: SoftwarePackage) -> bool:
cached_data = self.read(pkg)
if cached_data:
pkg.fill_cached_data(cached_data)
cache = self.cache_map.get(pkg.__class__)
if cache:
cache.add_non_existing(str(pkg.id), cached_data)
return True
return False
class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory):
def __init__(self, logger: logging.Logger):
super(DefaultDiskCacheLoaderFactory, self).__init__()
self.logger = logger
self.cache_map = {}
def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache):
if pkg_type:
if pkg_type not in self.cache_map:
self.cache_map[pkg_type] = cache
def new(self) -> AsyncDiskCacheLoader:
return AsyncDiskCacheLoader(cache_map=self.cache_map, logger=self.logger)

View File

@@ -1,22 +0,0 @@
import logging
from logging import INFO
FORMAT = '%(asctime)s %(levelname)s [%(module_path)s:%(lineno)s - %(funcName)s()] - %(message)s'
class FilePathFilter(logging.Filter):
def filter(self, record):
record.module_path = record.pathname.split('site-packages/')[1] if 'site-packages' in record.pathname else str(record.pathname)
return True
def new_logger(name: str, enabled: bool) -> logging.Logger:
instance = logging.Logger(name, level=INFO)
instance.addFilter(FilePathFilter())
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(FORMAT))
instance.addHandler(stream_handler)
instance.disabled = not enabled
return instance

View File

@@ -1,5 +0,0 @@
from bauh import ROOT_DIR
def get_path(resource_path):
return ROOT_DIR + '/view/resources/' + resource_path

View File

@@ -1,89 +0,0 @@
import glob
import locale
import os
from typing import Tuple, Set
from bauh.view.util import resource
class I18n(dict):
def __init__(self, current_key: str, current_locale: dict, default_key: str, default_locale: dict):
super(I18n, self).__init__()
self.current_key = current_key
self.current = current_locale
self.default_key = default_key
self.default = default_locale
def __getitem__(self, item):
try:
return self.current.__getitem__(item)
except KeyError:
if self.default:
try:
return self.default.__getitem__(item)
except KeyError:
return item
else:
return item
def get(self, *args, **kwargs):
res = self.current.get(args[0])
if res is None:
if self.default:
return self.default.get(*args, **kwargs)
else:
return self.current.get(*args, **kwargs)
return res
def get_available_keys() -> Set[str]:
locale_dir = resource.get_path('locale')
return {file.split('/')[-1] for file in glob.glob(locale_dir + '/*') if os.path.isfile(file)}
def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]:
locale_path = None
if key is None:
try:
current_locale = locale.getlocale()
if current_locale is None or current_locale[0] is None:
current_locale = ('en', 'UTF-8')
except Exception:
current_locale = ('en', 'UTF-8')
else:
current_locale = [key.strip().lower()]
if current_locale:
current_locale = current_locale[0]
for locale_file in glob.glob(locale_dir + '/*'):
name = locale_file.split('/')[-1]
if current_locale == name or current_locale.startswith(name + '_'):
locale_path = locale_file
break
if not locale_path:
return current_locale if current_locale else key, {}
with open(locale_path, 'r') as f:
locale_keys = f.readlines()
locale_obj = {}
for line in locale_keys:
line_strip = line.strip()
if line_strip:
try:
keyval = line_strip.split('=')
locale_obj[keyval[0].strip()] = keyval[1].strip()
except Exception:
print("Error decoding i18n line '{}'".format(line))
return locale_path.split('/')[-1], locale_obj

View File

@@ -1,88 +0,0 @@
import os
import shutil
import subprocess
import sys
import traceback
from typing import List, Tuple
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QIcon
from colorama import Fore
from bauh import __app_name__
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.paths import CONFIG_DIR, CACHE_DIR, TEMP_DIR
from bauh.commons.system import run_cmd
from bauh.view.util import resource
def notify_user(msg: str, icon_path: str = None):
icon_id = icon_path
if not icon_id:
icon_id = get_default_icon()[0]
os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_id) if icon_id else '', msg))
def get_default_icon(system: bool = True) -> Tuple[str, QIcon]:
if system:
system_icon = QIcon.fromTheme(__app_name__)
if not system_icon.isNull():
return system_icon.name(), system_icon
path = resource.get_path('img/logo.svg')
return path, QIcon(path)
def restart_app():
appimage_path = os.getenv('APPIMAGE')
restart_cmd = [appimage_path] if appimage_path else [sys.executable, *sys.argv]
subprocess.Popen(restart_cmd)
QCoreApplication.exit()
def get_distro():
if os.path.exists('/etc/arch-release'):
return 'arch'
if os.path.exists('/etc/os-release'):
with open('/etc/os-release', 'r') as os_release_file:
for line in os_release_file:
if 'ID_LIKE=arch' in line:
return 'arch'
if os.path.exists('/proc/version'):
if 'ubuntu' in run_cmd('cat /proc/version').lower():
return 'ubuntu'
return 'unknown'
def clean_app_files(managers: List[SoftwareManager], logs: bool = True):
if logs:
print('[{}] Cleaning configuration and cache files'.format(__app_name__))
for path in (CACHE_DIR, CONFIG_DIR, TEMP_DIR):
if logs:
print('[{}] Deleting directory {}'.format(__app_name__, path))
if os.path.exists(path):
try:
shutil.rmtree(path)
if logs:
print('{}[{}] Directory {} deleted{}'.format(Fore.YELLOW, __app_name__, path, Fore.RESET))
except Exception:
if logs:
print('{}[{}] An exception has happened when deleting {}{}'.format(Fore.RED, __app_name__, path, Fore.RESET))
traceback.print_exc()
if managers:
for m in managers:
m.clear_data()
if logs:
print('[{}] Cleaning finished'.format(__app_name__))

View File

@@ -1,2 +0,0 @@
from bauh.api import * # noqa: F401,F403

View File

@@ -1,2 +0,0 @@
from bauh.api.abstract import * # noqa: F401,F403

View File

@@ -1,2 +1,75 @@
from bauh.api.abstract.context import * # noqa: F401,F403 import logging
import sys
from typing import Optional, Dict
from bauh.api.abstract.cache import MemoryCacheFactory
from bauh.api.abstract.disk import DiskCacheLoaderFactory
from bauh.api.abstract.download import FileDownloader
from bauh.api.http import HttpClient
from bauh.commons.internet import InternetChecker
from bauh.view.util.translation import I18n
class ApplicationContext:
def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str,
app_version: str, internet_checker: InternetChecker, root_user: bool, screen_width: int = -1,
screen_height: int = -1, suggestions_mapping: Optional[Dict[str, str]] = None):
"""
:param download_icons: if packages icons should be downloaded
:param http_client: a shared instance of http client
:param app_root_dir: GUI root dir
:param i18n: the translation keys
:param cache_factory:
:param disk_loader_factory:
:param logger: a logger instance
:param file_downloader
:param distro
:param app_name
:param app_version
:param internet_checker
:param screen_width
:param screen_height
:param suggestions_mapping
"""
self.download_icons = download_icons
self.http_client = http_client
self.app_root_dir = app_root_dir
self.i18n = i18n
self.cache_factory = cache_factory
self.disk_loader_factory = disk_loader_factory
self.logger = logger
self.file_downloader = file_downloader
self.arch_x86_64 = sys.maxsize > 2**32
self.distro = distro
self.default_categories = ('AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System', 'Utility')
self.app_name = app_name
self.app_version = app_version
self.root_user = root_user
self.root_password = None
self.internet_checker = internet_checker
self.screen_width = screen_width
self.screen_height = screen_height
self._suggestions_mapping = suggestions_mapping
def is_system_x86_64(self):
return self.arch_x86_64
def get_view_path(self):
return self.app_root_dir + '/view'
def is_internet_available(self) -> bool:
return self.internet_checker.is_available()
def get_suggestion_url(self, module: str, default: Optional[str] = None) -> Optional[str]:
if self._suggestions_mapping:
module_split = module.split(f'{self.app_name}.gems.')
if len(module_split) > 1:
gem_name = module_split[1].split('.')[0]
return self._suggestions_mapping.get(gem_name, default)
return default

View File

@@ -1,2 +1,121 @@
from bauh.api.http import * # noqa: F401,F403 import logging
import time
import traceback
from typing import Optional
import requests
import yaml
from bauh.commons import system
from bauh.commons.view_utils import get_human_size_str
class HttpClient:
def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = 30, sleep: float = 0.5):
self.max_attempts = max_attempts
self.session = requests.Session()
self.timeout = timeout
self.sleep = sleep
self.logger = logger
def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False,
session: bool = True, stream: bool = False) -> Optional[requests.Response]:
cur_attempts = 1
while cur_attempts <= self.max_attempts:
cur_attempts += 1
try:
args = {'timeout': self.timeout, 'allow_redirects': allow_redirects, 'stream': stream}
if params:
args['params'] = params
if headers:
args['headers'] = headers
if ignore_ssl:
args['verify'] = False
if session:
res = self.session.get(url, **args)
else:
res = requests.get(url, **args)
if 200 <= res.status_code < 300:
return res
if single_call:
return res
if self.sleep > 0:
time.sleep(self.sleep)
except Exception as e:
if isinstance(e, requests.exceptions.ConnectionError):
self.logger.error('Internet seems to be off')
raise e
elif isinstance(e, requests.exceptions.TooManyRedirects):
self.logger.warning(f"Too many redirects for GET -> {url}")
raise e
elif e.__class__ in (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema):
self.logger.warning(f"The URL '{url}' has an invalid schema")
raise e
self.logger.error(f"Could not retrieve data from '{url}'")
traceback.print_exc()
continue
self.logger.warning(f"Could not retrieve data from '{url}'")
def get_json(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
return res.json() if res else None
def get_yaml(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, session: bool = True):
res = self.get(url, params=params, headers=headers, allow_redirects=allow_redirects, session=session)
return yaml.safe_load(res.text) if res else None
def get_content_length_in_bytes(self, url: str, session: bool = True) -> Optional[int]:
if not url:
return
params = {'url': url, 'allow_redirects': True, 'stream': True}
try:
if session:
res = self.session.get(**params)
else:
res = requests.get(**params)
except requests.exceptions.ConnectionError:
self.logger.info(f"Internet seems to be off. Could not reach '{url}'")
return
if res.status_code == 200:
size = res.headers.get('Content-Length')
if size:
try:
return int(size)
except Exception:
pass
def get_content_length(self, url: str, session: bool = True) -> Optional[str]:
size = self.get_content_length_in_bytes(url, session)
if size:
return get_human_size_str(size)
def exists(self, url: str, session: bool = True, timeout: int = 5) -> bool:
params = {'url': url, 'allow_redirects': True, 'verify': False, 'timeout': timeout}
try:
if session:
res = self.session.head(**params)
else:
res = self.session.get(**params)
except requests.exceptions.TooManyRedirects:
self.logger.warning(f"{url} seems to exist, but too many redirects have happened")
return True
return res.status_code in (200, 403)

View File

@@ -1,2 +1,70 @@
from bauh.api.paths import * # noqa: F401,F403 import os
import shutil
from getpass import getuser
from pathlib import Path
from bauh import __app_name__
from bauh.api import user
def get_temp_dir(username: str) -> str:
return f'/tmp/{__app_name__}@{username}'
CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}'
CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}'
USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes'
DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications'
TEMP_DIR = get_temp_dir(getuser())
LOGS_DIR = f'{TEMP_DIR}/logs'
AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart'
BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin'
SHARED_FILES_DIR = f'/usr/local/share/{__app_name__}' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}'
def _migrate_dir_if_possible(old_path: str, new_path: str):
if old_path == new_path:
return
if not os.path.exists(old_path):
return
if os.path.exists(new_path):
return
try:
parent = os.path.dirname(new_path)
if parent:
Path(parent).mkdir(parents=True, exist_ok=True)
shutil.move(old_path, new_path)
except Exception:
# The migration is best-effort. If it fails, the app can still start with empty defaults.
pass
def migrate_legacy_paths():
old_app_name = 'bauh'
if old_app_name == __app_name__:
return
if user.is_root():
old_cache = f'/var/cache/{old_app_name}'
old_config = f'/etc/{old_app_name}'
old_shared = f'/usr/local/share/{old_app_name}'
old_temp = f'/tmp/{old_app_name}@{getuser()}'
else:
home = Path.home()
old_cache = f'{home}/.cache/{old_app_name}'
old_config = f'{home}/.config/{old_app_name}'
old_shared = f'{home}/.local/share/{old_app_name}'
old_temp = f'/tmp/{old_app_name}@{getuser()}'
_migrate_dir_if_possible(old_cache, CACHE_DIR)
_migrate_dir_if_possible(old_config, CONFIG_DIR)
_migrate_dir_if_possible(old_shared, SHARED_FILES_DIR)
_migrate_dir_if_possible(old_temp, TEMP_DIR)
migrate_legacy_paths()

View File

@@ -1,2 +1,6 @@
from bauh.api.user import * # noqa: F401,F403 import os
from typing import Optional
def is_root(user_id: Optional[int] = None):
return user_id == 0 if user_id is not None else os.getuid() == 0

View File

@@ -1,2 +0,0 @@
from bauh.commons import * # noqa: F401,F403

View File

@@ -1,2 +1,102 @@
from bauh.commons.config import * # noqa: F401,F403 import os
import traceback
from abc import abstractmethod, ABC
from pathlib import Path
from threading import Thread
from typing import Optional
import yaml
from bauh.api.paths import CONFIG_DIR
from bauh.commons import util
def read_config(file_path: str, template: dict, update_file: bool = False, update_async: bool = False) -> dict:
if not os.path.exists(file_path):
Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True)
save_config(template, file_path)
else:
with open(file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config:
util.deep_update(template, local_config)
if update_file:
if update_async:
Thread(target=save_config, args=(template, file_path), daemon=True).start()
else:
save_config(template, file_path)
return template
def save_config(config: dict, file_path: str):
with open(file_path, 'w+') as f:
f.write(yaml.dump(config))
class ConfigManager(ABC):
@abstractmethod
def read_config(self) -> Optional[dict]:
pass
@abstractmethod
def get_default_config(self) -> dict:
pass
@abstractmethod
def is_config_cached(self) -> bool:
pass
def get_config(self) -> dict:
default_config = self.get_default_config()
if default_config:
cached_config = self.read_config()
if cached_config:
self.merge_config(default_config, cached_config)
return default_config
@staticmethod
def merge_config(base_config: dict, current_config: dict):
util.deep_update(base_config, current_config)
@abstractmethod
def save_config(self, config_obj: dict):
pass
class YAMLConfigManager(ConfigManager, ABC):
def __init__(self, config_file_path: str):
self.file_path = config_file_path
def is_config_cached(self) -> bool:
return os.path.exists(self.file_path)
def read_config(self) -> Optional[dict]:
if self.is_config_cached():
with open(self.file_path) as f:
local_config = yaml.safe_load(f.read())
if local_config is not None:
return local_config
def save_config(self, config_obj: dict):
if config_obj:
config_dir = os.path.dirname(self.file_path)
try:
Path(config_dir).mkdir(parents=True, exist_ok=True)
except OSError:
traceback.print_exc()
return
try:
with open(self.file_path, 'w+') as f:
f.write(yaml.dump(config_obj))
except Exception:
traceback.print_exc()

View File

@@ -1,2 +1,17 @@
from bauh.commons.internet import * # noqa: F401,F403 import socket
class InternetChecker:
def __init__(self, offline: bool):
self.offline = offline
def is_available(self) -> bool:
if self.offline:
return False
try:
socket.gethostbyname("w3.org")
return True
except Exception:
return False

View File

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Some files were not shown because too many files have changed in this diff Show More