arch:using axel to pre-download source file | info window handling list attributes better

This commit is contained in:
Vinicius Moreira
2019-09-22 01:01:11 -03:00
parent 0085f40cdf
commit 917c6ec5d7
18 changed files with 189 additions and 22 deletions

View File

@@ -29,7 +29,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Progress bar status can now be controlled by the software manager while an operation is being executed - Progress bar status can now be controlled by the software manager while an operation is being executed
- Flatpak: showing runtime branches as versions when they are not available - Flatpak: showing runtime branches as versions when they are not available
- small UI improvements - small UI improvements
- installation logs are saved at **/tmp/bauh/logs/install**
- Environment variable / parameter BAUH_UPDATE_NOTIFICATION renamed to BAUH_SYSTEM_NOTIFICATIONS and now works for any system notification - Environment variable / parameter BAUH_UPDATE_NOTIFICATION renamed to BAUH_SYSTEM_NOTIFICATIONS and now works for any system notification
- Environment variable / parameter BAUH_DOWNLOAD_MULTITHREAD (--download-mthread): if source files should be downloaded using multi-threads.
### UI Changes ### UI Changes
- **Upgrade selected** and **Refresh** buttons now have text labels and new colors - **Upgrade selected** and **Refresh** buttons now have text labels and new colors
@@ -45,6 +47,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Ubuntu root password check - Ubuntu root password check
- [Ubuntu 19.04 pip3 install issue] (https://github.com/vinifmor/bauh/issues/3) - [Ubuntu 19.04 pip3 install issue] (https://github.com/vinifmor/bauh/issues/3)
### AUR support (**arch gem**):
- Search, install, uninstall, downgrade, retrieve history and launch packages
- Faster source files download when (**axel**) [https://github.com/axel-download-accelerator/axel] is installed on your system
### Code ### Code
- Code was internally modularized as: **api** (conceptual classes used to create custom software managers), **gems** (software managers), **commons** (common classes shared between the **view** and **gems**), **view** (UI code) - Code was internally modularized as: **api** (conceptual classes used to create custom software managers), **gems** (software managers), **commons** (common classes shared between the **view** and **gems**), **view** (UI code)
- **api** allows custom operations, so the **gems** can provide actions that the current GUI does not support (Snap "refresh" was refactored as a custom operation) - **api** allows custom operations, so the **gems** can provide actions that the current GUI does not support (Snap "refresh" was refactored as a custom operation)

View File

@@ -72,7 +72,7 @@ You can change some application settings via environment variables or arguments
- **BAUH_SUGGESTIONS**: If application suggestions should be displayed if no packaged considered as an application is installed (runtimes / libraries do not count as applications). Use **0** (disable) or **1** (enable, default). - **BAUH_SUGGESTIONS**: If application suggestions should be displayed if no packaged considered as an application is installed (runtimes / libraries do not count as applications). Use **0** (disable) or **1** (enable, default).
- **BAUH_MAX_DISPLAYED**: Maximum number of displayed packages in the management panel table. Default: 50. - **BAUH_MAX_DISPLAYED**: Maximum number of displayed packages in the management panel table. Default: 50.
- **BAUH_LOGS**: enable **bauh** logs (for debugging purposes). Use: **0** (disable, default) or **1** (enable) - **BAUH_LOGS**: enable **bauh** logs (for debugging purposes). Use: **0** (disable, default) or **1** (enable)
- **BAUH_DOWNLOAD_MULTITHREAD**: enable multi-threaded download for installation files ( only possible if **axel** is installed ). Use **0** (disable) or **1** (enabled, default).
The application settings are stored in **/home/$USER/.config/bauh/config.json** The application settings are stored in **/home/$USER/.config/bauh/config.json**
@@ -87,6 +87,8 @@ The application settings are stored in **/home/$USER/.config/bauh/config.json**
- If you don't mind app suggestions, disable it (**bauh --sugs=0**) - If you don't mind app suggestions, disable it (**bauh --sugs=0**)
- Let the disk cache always enabled so **bauh** does not need to dynamically retrieve some data every time you launch it. - Let the disk cache always enabled so **bauh** does not need to dynamically retrieve some data every time you launch it.
### Logs
- Installation logs are saved at **/tmp/bauh/logs/install**
### Code structure ### Code structure
#### Modules #### Modules

View File

@@ -1,8 +1,10 @@
import logging import logging
import platform import platform
import sys
from bauh.api.abstract.cache import MemoryCacheFactory from bauh.api.abstract.cache import MemoryCacheFactory
from bauh.api.abstract.disk import DiskCacheLoaderFactory from bauh.api.abstract.disk import DiskCacheLoaderFactory
from bauh.api.abstract.download import FileDownloader
from bauh.api.http import HttpClient from bauh.api.http import HttpClient
@@ -10,7 +12,7 @@ class ApplicationContext:
def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: dict, def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: dict,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory, cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger): logger: logging.Logger, file_downloader: FileDownloader):
""" """
:param disk_cache: if package data should be cached to disk :param disk_cache: if package data should be cached to disk
:param download_icons: if packages icons should be downloaded :param download_icons: if packages icons should be downloaded
@@ -20,6 +22,7 @@ class ApplicationContext:
:param cache_factory: :param cache_factory:
:param disk_loader_factory: :param disk_loader_factory:
:param logger: a logger instance :param logger: a logger instance
:param file_downloader:
""" """
self.disk_cache = disk_cache self.disk_cache = disk_cache
self.download_icons = download_icons self.download_icons = download_icons
@@ -30,3 +33,8 @@ class ApplicationContext:
self.disk_loader_factory = disk_loader_factory self.disk_loader_factory = disk_loader_factory
self.logger = logger self.logger = logger
self.linux_distro = platform.linux_distribution() self.linux_distro = platform.linux_distribution()
self.file_downloader = file_downloader
self.arch_x86_64 = sys.maxsize > 2**32
def is_system_x86_64(self):
return self.arch_x86_64

View File

@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from bauh.api.abstract.handler import ProcessWatcher
class FileDownloader(ABC):
@abstractmethod
def download(self, file_url: str, watcher: ProcessWatcher, output_path: str, cwd: str) -> bool:
"""
:param file_url:
:param watcher:
:param output_path: the downloaded file output path. Leave None for the current directory and the same file name
:param cwd: current working directory. Leave None if does not matter.
:return: success / failure
"""
pass
@abstractmethod
def is_multithreaded(self) -> bool:
pass

View File

@@ -38,3 +38,13 @@ class HttpClient:
def get_json(self, url: str): def get_json(self, url: str):
res = self.get(url) res = self.get(url)
return res.json() if res else None return res.json() if res else None
def get_content_length(self, url: str) -> int:
"""
:param url:
:return:
"""
res = self.session.head(url)
if res.status_code == 200:
return res.headers['content-length']

View File

@@ -8,6 +8,7 @@ from bauh.api.abstract.controller import ApplicationContext
from bauh.api.http import HttpClient from bauh.api.http import HttpClient
from bauh.view.core import gems, config from bauh.view.core import gems, config
from bauh.view.core.controller import GenericSoftwareManager from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.core.downloader import AdaptableFileDownloader
from bauh.view.util import util, logs, resource from bauh.view.util import util, logs, resource
from bauh.view.qt.systray import TrayIcon from bauh.view.qt.systray import TrayIcon
from bauh.view.qt.window import ManageWindow from bauh.view.qt.window import ManageWindow
@@ -33,7 +34,8 @@ def main():
app_root_dir=ROOT_DIR, app_root_dir=ROOT_DIR,
cache_factory=cache_factory, cache_factory=cache_factory,
disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache, logger=logger), disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache, logger=logger),
logger=logger) logger=logger,
file_downloader=AdaptableFileDownloader(logger, bool(args.download_mthread)))
user_config = config.read() user_config = config.read()
app = QApplication(sys.argv) app = QApplication(sys.argv)

View File

@@ -36,6 +36,7 @@ def read() -> Namespace:
parser.add_argument('-md', '--max-displayed', action="store", default=os.getenv('BAUH_MAX_DISPLAYED', 50), type=int, help='Maximum number of displayed packages in the management panel table. Default: %(default)s') parser.add_argument('-md', '--max-displayed', action="store", default=os.getenv('BAUH_MAX_DISPLAYED', 50), type=int, help='Maximum number of displayed packages in the management panel table. Default: %(default)s')
parser.add_argument('--logs', action="store", default=int(os.getenv('BAUH_LOGS', 0)), choices=[0, 1], type=int, help='If the application logs should be displayed. Default: %(default)s') parser.add_argument('--logs', action="store", default=int(os.getenv('BAUH_LOGS', 0)), choices=[0, 1], type=int, help='If the application logs should be displayed. Default: %(default)s')
parser.add_argument('--show-panel', action="store_true", help='Shows the management panel after the app icon is attached to the tray.') parser.add_argument('--show-panel', action="store_true", help='Shows the management panel after the app icon is attached to the tray.')
parser.add_argument('-dm', '--download-mthread', action="store", default=os.getenv('BAUH_DOWNLOAD_MULTITHREAD', 1), choices=[0, 1], type=int, help='If installation files should be downloaded using multi-threads (only possible if axel is installed). Default: %(default)s')
return parser.parse_args() return parser.parse_args()
@@ -70,4 +71,7 @@ def validate(args: Namespace, logger: logging.Logger):
if args.logs == 1: if args.logs == 1:
logger.info("Logs are enabled") logger.info("Logs are enabled")
if args.download_mthread == 0:
logger.info("Multithreaded downloads disabled")
return args return args

View File

@@ -11,6 +11,8 @@ URL_SEARCH = 'https://aur.archlinux.org/rpc/?v=5&type=search&arg='
RE_SRCINFO_KEYS = re.compile(r'(\w+)\s+=\s+(.+)\n') RE_SRCINFO_KEYS = re.compile(r'(\w+)\s+=\s+(.+)\n')
KNOWN_LIST_FIELDS = ('validpgpkeys', 'depends', 'optdepends', 'sha512sums', 'sha512sums_x86_64', 'source', 'source_x86_64')
def map_pkgbuild(pkgbuild: str) -> dict: def map_pkgbuild(pkgbuild: str) -> dict:
return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)} return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)}
@@ -36,16 +38,13 @@ class AURClient:
info = {} info = {}
for field in RE_SRCINFO_KEYS.findall(res.text): for field in RE_SRCINFO_KEYS.findall(res.text):
if field[0] not in info: if field[0] not in info:
info[field[0]] = field[1] info[field[0]] = [field[1]] if field[0] in KNOWN_LIST_FIELDS else field[1]
else: else:
if not isinstance(info[field[0]], list): if not isinstance(info[field[0]], list):
info[field[0]] = [info[field[0]]] info[field[0]] = [info[field[0]]]
info[field[0]].append(field[1]) info[field[0]].append(field[1])
if info.get('validpgpkeys') and isinstance(info['validpgpkeys'], str):
info['validpgpkeys'] = [info['validpgpkeys']]
return info return info
def _map_names_as_queries(self, names) -> str: def _map_names_as_queries(self, names) -> str:

View File

@@ -26,6 +26,9 @@ URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
RE_SPLIT_VERSION = re.compile(r'(=|>|<)') RE_SPLIT_VERSION = re.compile(r'(=|>|<)')
SOURCE_FIELDS = ('source', 'source_x86_64')
RE_PRE_DOWNLOADABLE_FILES = re.compile(r'(https?|ftp)://.+\.\w+[^gpg]$')
class ArchManager(SoftwareManager): class ArchManager(SoftwareManager):
@@ -251,7 +254,7 @@ class ArchManager(SoftwareManager):
'07_maintainer': pkg.maintainer, '07_maintainer': pkg.maintainer,
'08_first_submitted': pkg.first_submitted, '08_first_submitted': pkg.first_submitted,
'09_last_modified': pkg.last_modified, '09_last_modified': pkg.last_modified,
'10_url': pkg.url_download, '10_url': pkg.url_download
} }
srcinfo = self.aur_client.get_src_info(pkg.name) srcinfo = self.aur_client.get_src_info(pkg.name)
@@ -344,7 +347,43 @@ class ArchManager(SoftwareManager):
return pkg_mirrors return pkg_mirrors
def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessWatcher) -> bool:
if self.context.file_downloader.is_multithreaded():
srcinfo = self.aur_client.get_src_info(pkgname)
pre_download_files = []
for attr in SOURCE_FIELDS:
if srcinfo.get(attr):
if attr == 'source_x86_x64' and not self.context.is_system_x86_64():
continue
else:
for f in srcinfo[attr]:
if RE_PRE_DOWNLOADABLE_FILES.findall(f):
pre_download_files.append(f)
for f in pre_download_files:
fdata = f.split('::')
args = {'watcher': watcher, 'cwd': project_dir}
if len(fdata) > 1:
args.update({'file_url': fdata[1], 'output_path': fdata[0]})
else:
args.update({'file_url': fdata[0], 'output_path': None})
file_size = self.context.http_client.get_content_length(args['file_url'])
file_size = int(file_size) / (1024 ** 2) if file_size else None
watcher.change_substatus(self.i18n['downloading'] + ' ' + bold(args['file_url'].split('/')[-1]) + ' ( {0:.2f} Mb )'.format(file_size) if file_size else '')
if not self.context.file_downloader.download(**args):
watcher.print('Could not download source file {}'.format(args['file_url']))
return False
return True
def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool: def _make_pkg(self, pkgname: str, root_password: str, handler: ProcessHandler, build_dir: str, project_dir: str, dependency: bool, skip_optdeps: bool = False, change_progress: bool = True) -> bool:
if not self._pre_download_source(pkgname, project_dir, handler.watcher):
return False
self._update_progress(handler.watcher, 50, change_progress) self._update_progress(handler.watcher, 50, change_progress)
if not self._install_missings_deps_and_keys(pkgname, root_password, handler, project_dir): if not self._install_missings_deps_and_keys(pkgname, root_password, handler, project_dir):
@@ -378,7 +417,7 @@ class ArchManager(SoftwareManager):
def _install_missings_deps_and_keys(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool: def _install_missings_deps_and_keys(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str) -> bool:
handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname))) handler.watcher.change_substatus(self.i18n['arch.checking.deps'].format(bold(pkgname)))
check_res = makepkg.check_missing_deps(pkgdir, handler.watcher) check_res = makepkg.check(pkgdir, handler.watcher)
if check_res: if check_res:
if check_res.get('gpg_key'): if check_res.get('gpg_key'):

View File

@@ -7,35 +7,39 @@ RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n')
RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)') RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)')
def check_missing_deps(pkgdir: str, watcher: ProcessWatcher) -> dict: def check(pkgdir: str, watcher: ProcessWatcher) -> dict:
depcheck = new_subprocess(['makepkg', '-L', '--check'], cwd=pkgdir) depcheck = new_subprocess(['makepkg', '-ALcf', '--check', '--noarchive'], cwd=pkgdir)
res = {} res = {}
output_lines = []
for o in depcheck.stdout: for o in depcheck.stdout:
if o: if o:
line = o.decode().strip() line = o.decode().strip()
if line: if line:
output_lines.append(line)
watcher.print(line) watcher.print(line)
error_lines = []
for s in depcheck.stderr: for s in depcheck.stderr:
if s: if s:
line = s.decode() line = s.decode()
if line: if line:
error_lines.append(line) output_lines.append(line)
line_strip = line.strip() line_strip = line.strip()
if line_strip: if line_strip:
watcher.print(line) watcher.print(line_strip)
if error_lines: if output_lines:
error_str = ''.join(error_lines) error_str = ''.join(output_lines)
if 'Missing dependencies' in error_str: if 'Missing dependencies' in error_str:
res['missing_deps'] = RE_DEPS_PATTERN.findall(error_str) res['missing_deps'] = RE_DEPS_PATTERN.findall(error_str)
gpg_keys = RE_UNKNOWN_GPG_KEY.findall(error_str) gpg_keys = RE_UNKNOWN_GPG_KEY.findall(error_str)
if gpg_keys: if gpg_keys:
res['gpg_key'] = gpg_keys[0] res['gpg_key'] = gpg_keys[0]

View File

@@ -61,6 +61,8 @@ def get_info_dict(pkg_name: str) -> dict:
if attr == 'optional deps' and info_dict[attr]: if attr == 'optional deps' and info_dict[attr]:
info_dict[attr] = RE_DEPS.findall(info_dict[attr]) info_dict[attr] = RE_DEPS.findall(info_dict[attr])
elif attr == 'depends on' and info_dict[attr]:
info_dict[attr] = [d.strip() for d in info_dict[attr].split(' ') if d]
return info_dict return info_dict

View File

@@ -218,7 +218,7 @@ class GenericSoftwareManager(SoftwareManager):
return False return False
finally: finally:
tf = time.time() tf = time.time()
self.logger.info('Installation of {} took {} seconds'.format(app, tf - ti)) self.logger.info('Installation of {}'.format(app) + 'took {0:.2f} minutes'.format((tf - ti)/60))
def get_info(self, app: SoftwarePackage): def get_info(self, app: SoftwarePackage):
man = self._get_manager_for(app) man = self._get_manager_for(app)

View File

@@ -0,0 +1,53 @@
import logging
import time
from multiprocessing import cpu_count
from typing import List
from bauh.api.abstract.download import FileDownloader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.commons.system import run_cmd, new_subprocess, ProcessHandler, SystemProcess
class AdaptableFileDownloader(FileDownloader):
def __init__(self, logger: logging.Logger, multithread_enabled: bool):
self.download_threads = cpu_count() * 2
self.logger = logger
self.multithread_enabled = multithread_enabled
def is_axel_available(self) -> bool:
return bool(run_cmd('which axel'))
def _get_download_command(self, url: str, output_path: str) -> List:
if self.is_multithreaded():
cmd = ['axel', '-k', '-n', str(self.download_threads), url]
if output_path:
cmd.append('-o')
cmd.append(output_path)
return cmd
cmd = ['wget', url]
if output_path:
cmd.append('-O')
cmd.append(output_path)
return cmd
def download(self, file_url: str, watcher: ProcessWatcher, output_path: str, cwd: str):
handler = ProcessHandler(watcher)
cmd = self._get_download_command(file_url, output_path)
self.logger.info('Downloading {}'.format(file_url))
watcher.print("[{}] downloading {}{}".format(cmd[0], file_url, " as {}".format(output_path) if output_path else ''))
ti = time.time()
res = handler.handle(SystemProcess(new_subprocess(cmd=cmd, cwd=cwd if cwd else '.')))
tf = time.time()
self.logger.info(file_url.split('/')[-1] + ' download took {0:.2f} minutes'.format((tf - ti) / 60))
return res
def is_multithreaded(self) -> bool:
return self.multithread_enabled and self.is_axel_available()

View File

@@ -12,7 +12,7 @@ class InfoDialog(QDialog):
def __init__(self, app: dict, icon_cache: MemoryCache, locale_keys: dict, screen_size: QSize()): def __init__(self, app: dict, icon_cache: MemoryCache, locale_keys: dict, screen_size: QSize()):
super(InfoDialog, self).__init__() super(InfoDialog, self).__init__()
self.setWindowTitle(app['__app__'].model.name) self.setWindowTitle(str(app['__app__']))
self.screen_size = screen_size self.screen_size = screen_size
self.i18n = locale_keys self.i18n = locale_keys
layout = QVBoxLayout() layout = QVBoxLayout()
@@ -50,7 +50,7 @@ class InfoDialog(QDialog):
i18n_key = app['__app__'].model.get_type() + '.info.' + attr.lower() i18n_key = app['__app__'].model.get_type() + '.info.' + attr.lower()
if isinstance(app[attr], list): if isinstance(app[attr], list):
val = '\n'.join([str(e) for e in app[attr]]) val = '\n'.join(['* ' + str(e.strip()) for e in app[attr] if e])
else: else:
val = str(app[attr]).strip() val = str(app[attr]).strip()

View File

@@ -1,5 +1,7 @@
import operator import operator
import time
from functools import reduce from functools import reduce
from pathlib import Path
from typing import List, Type, Set from typing import List, Type, Set
from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
@@ -867,6 +869,18 @@ class ManageWindow(QWidget):
self.thread_install.start() self.thread_install.start()
def _finish_install(self, pkgv: PackageView): def _finish_install(self, pkgv: PackageView):
console_output = self.textarea_output.toPlainText()
if console_output:
log_path = '/tmp/bauh/logs/install/{}/{}'.format(pkgv.model.get_type(), pkgv.model.name)
try:
Path(log_path).mkdir(parents=True, exist_ok=True)
with open(log_path + '/{}.log'.format(int(time.time())), 'w+') as f:
f.write(console_output)
except:
self.textarea_output.appendPlainText("[warning] Could not write install log file to '{}'".format(log_path))
self.input_search.setText('') self.input_search.setText('')
self.finish_action() self.finish_action()

View File

@@ -115,4 +115,5 @@ manage_window.settings.gems=Application types
style=style style=style
style.change.title=Style change style.change.title=Style change
style.change.question=To change the current style is necessary to restart {}. Proceed ? style.change.question=To change the current style is necessary to restart {}. Proceed ?
manage_window.bt_settings.tooltip=Click here to open extra actions manage_window.bt_settings.tooltip=Click here to open extra actions
downloading=Downloading

View File

@@ -117,4 +117,5 @@ manage_window.settings.gems=Tipos de aplicativos
style=estilo style=estilo
style.change.title=Cambio de estilo style.change.title=Cambio de estilo
style.change.question=Para cambiar el estilo actual es necesario reiniciar {}. ¿Proceder? style.change.question=Para cambiar el estilo actual es necesario reiniciar {}. ¿Proceder?
manage_window.bt_settings.tooltip=Haga clic aquí para abrir acciones adicionales manage_window.bt_settings.tooltip=Haga clic aquí para abrir acciones adicionales
downloading=Descargando

View File

@@ -117,4 +117,5 @@ manage_window.settings.gems=Tipos de aplicativos
style=estilo style=estilo
style.change.title=Mudança de estilo style.change.title=Mudança de estilo
style.change.question=Para alterar o estilo atual é necessário reiniciar o {}. Continuar ? style.change.question=Para alterar o estilo atual é necessário reiniciar o {}. Continuar ?
manage_window.bt_settings.tooltip=Clique aqui para abrir ações adicionais manage_window.bt_settings.tooltip=Clique aqui para abrir ações adicionais
downloading=Baixando