mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
Introduce framework for checking/installing packages
- Uses PackageKit dameon, Glib library wrapping packagekit DBUS API and Python bindings for the Glib library. - Implement a decorator to wrap views requiring packages. - Framework allows for parallel operations. However, doing parallel operations hangs because of what appears to be PackageKit backend limitations.
This commit is contained in:
parent
7680d398a6
commit
a4be460538
2
INSTALL
2
INSTALL
@ -7,7 +7,7 @@
|
||||
$ sudo apt-get install libjs-jquery libjs-modernizr \
|
||||
libjs-bootstrap make pandoc python3 python3-cherrypy3 \
|
||||
python3-coverage python3-django python3-bootstrapform \
|
||||
python3-setuptools
|
||||
python3-gi python3-setuptools gir1.2-packagekitglib-1.0
|
||||
|
||||
2. Install Plinth:
|
||||
|
||||
|
||||
33
plinth/forms.py
Normal file
33
plinth/forms.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Plinth framework forms
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
|
||||
|
||||
class PackageInstallForm(forms.Form):
|
||||
"""Prompt for installation of a package.
|
||||
|
||||
XXX: Don't store the package list in a hidden input as it can be
|
||||
modified on the client side. Use session store to store and retrieve
|
||||
the package list. It has to be form specific so that multiple
|
||||
instances of forms don't clash with each other.
|
||||
"""
|
||||
package_names = forms.CharField(widget=forms.HiddenInput)
|
||||
184
plinth/package.py
Normal file
184
plinth/package.py
Normal file
@ -0,0 +1,184 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Framework for installing and updating distribution packages
|
||||
"""
|
||||
|
||||
import functools
|
||||
from gi.repository import PackageKitGlib as packagekit
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import plinth
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
transactions = {}
|
||||
packages_resolved = {}
|
||||
|
||||
|
||||
class Transaction(object):
|
||||
"""Information about an ongoing transaction."""
|
||||
|
||||
def __init__(self, package_names):
|
||||
"""Initialize transaction object.
|
||||
|
||||
Set most values to None until they are sent as progress update.
|
||||
"""
|
||||
self.package_names = package_names
|
||||
|
||||
# Progress
|
||||
self.allow_cancel = None
|
||||
self.percentage = None
|
||||
self.status = None
|
||||
self.status_string = None
|
||||
self.flags = None
|
||||
self.package = None
|
||||
self.package_id = None
|
||||
self.item_progress = None
|
||||
self.role = None
|
||||
self.caller_active = None
|
||||
self.download_size_remaining = None
|
||||
|
||||
def get_id(self):
|
||||
"""Return a identifier to use as a key in a map of transactions."""
|
||||
return frozenset(self.package_names)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the string representation of the object"""
|
||||
return ('Transaction(packages={0}, allow_cancel={1}, status={2}, '
|
||||
' percentage={3}, package={4}, item_progress={5})').format(
|
||||
self.package_names, self.allow_cancel, self.status_string,
|
||||
self.percentage, self.package, self.item_progress)
|
||||
|
||||
def start_install(self):
|
||||
"""Start a PackageKit transaction to install given list of packages.
|
||||
|
||||
This operation is non-blocking at it spawns a new thread.
|
||||
"""
|
||||
thread = threading.Thread(target=self._install)
|
||||
thread.start()
|
||||
|
||||
def _install(self):
|
||||
"""Run a PackageKit transaction to install given packages."""
|
||||
package_ids = [packages_resolved[package_name].get_id()
|
||||
for package_name in self.package_names]
|
||||
client = packagekit.Client()
|
||||
client.set_interactive(False)
|
||||
client.install_packages(packagekit.TransactionFlagEnum.ONLY_TRUSTED,
|
||||
package_ids + [None], None,
|
||||
self.progress_callback, self)
|
||||
|
||||
def progress_callback(self, progress, progress_type, user_data):
|
||||
"""Process progress updates on package resolve operation"""
|
||||
if progress_type == packagekit.ProgressType.PERCENTAGE:
|
||||
self.percentage = progress.props.percentage
|
||||
elif progress_type == packagekit.ProgressType.PACKAGE:
|
||||
self.package = progress.props.package
|
||||
elif progress_type == packagekit.ProgressType.ALLOW_CANCEL:
|
||||
self.allow_cancel = progress.props.allow_cancel
|
||||
elif progress_type == packagekit.ProgressType.PACKAGE_ID:
|
||||
self.package_id = progress.props.package_id
|
||||
elif progress_type == packagekit.ProgressType.ITEM_PROGRESS:
|
||||
self.item_progress = progress.props.item_progress
|
||||
elif progress_type == packagekit.ProgressType.STATUS:
|
||||
self.status = progress.props.status
|
||||
self.status_string = \
|
||||
packagekit.StatusEnum.to_string(progress.props.status)
|
||||
if self.status == packagekit.StatusEnum.FINISHED:
|
||||
self.finish()
|
||||
elif progress_type == packagekit.ProgressType.TRANSACTION_FLAGS:
|
||||
self.flags = progress.props.transaction_flags
|
||||
elif progress_type == packagekit.ProgressType.ROLE:
|
||||
self.role = progress.props.role
|
||||
elif progress_type == packagekit.ProgressType.CALLER_ACTIVE:
|
||||
self.caller_active = progress.props.caller_active
|
||||
elif progress_type == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING:
|
||||
self.download_size_remaining = \
|
||||
progress.props.download_size_remaining
|
||||
else:
|
||||
logger.info('Unhandle packagekit progress callback - %s, %s',
|
||||
progress, progress_type)
|
||||
|
||||
def finish(self):
|
||||
"""Perform clean up operations on the transaction.
|
||||
|
||||
Remove self from global transactions list.
|
||||
"""
|
||||
del transactions[self.get_id()]
|
||||
|
||||
|
||||
def required(*package_names):
|
||||
"""Decorate a view to check and install required packages."""
|
||||
|
||||
def wrapper2(func):
|
||||
"""Return a function to check and install packages."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
"""Check and install packages required by a view."""
|
||||
if not is_installing(package_names) and \
|
||||
check_installed(package_names):
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
view = plinth.views.PackageInstallView.as_view()
|
||||
return view(request, package_names=package_names, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return wrapper2
|
||||
|
||||
|
||||
def check_installed(package_names):
|
||||
"""Return a boolean installed status of package.
|
||||
|
||||
This operation is blocking and waits until the check is finished.
|
||||
"""
|
||||
def _callback(progress, progress_type, user_data):
|
||||
"""Process progress updates on package resolve operation."""
|
||||
pass
|
||||
|
||||
client = packagekit.Client()
|
||||
response = client.resolve(packagekit.FilterEnum.INSTALLED,
|
||||
package_names + (None, ), None,
|
||||
_callback, None)
|
||||
|
||||
installed_package_names = []
|
||||
for package in response.get_package_array():
|
||||
if package.get_info() == packagekit.InfoEnum.INSTALLED:
|
||||
installed_package_names.append(package.get_name())
|
||||
|
||||
packages_resolved[package.get_name()] = package
|
||||
|
||||
return set(installed_package_names) == set(package_names)
|
||||
|
||||
|
||||
def is_installing(package_names):
|
||||
"""Return whether a set of packages are currently being installed."""
|
||||
return frozenset(package_names) in transactions
|
||||
|
||||
|
||||
def start_install(package_names):
|
||||
"""Start a PackageKit transaction to install given list of packages.
|
||||
|
||||
This operation is non-blocking at it spawns a new thread.
|
||||
"""
|
||||
transaction = Transaction(package_names)
|
||||
transactions[frozenset(package_names)] = transaction
|
||||
|
||||
transaction.start_install()
|
||||
82
plinth/templates/package_install.html
Normal file
82
plinth/templates/package_install.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
|
||||
{% block page_head %}
|
||||
|
||||
{% if is_installing %}
|
||||
<meta http-equiv="refresh" content="3" />
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Installation</h2>
|
||||
|
||||
{% if not is_installing %}
|
||||
|
||||
<p>This feature requires addtional packages to be installed. Do you
|
||||
wish to install them?</p>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Package</th><th>Summary</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for package in packages %}
|
||||
<tr>
|
||||
<td>{{ package.get_name }}</td>
|
||||
<td>{{ package.get_summary }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-md btn-primary" value="Install" />
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% for key, transaction in transactions.items %}
|
||||
<div>Installing {{ transaction.package_names|join:", " }}:
|
||||
{{ transaction.status_string }}
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active"
|
||||
role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuenow="{{ transaction.percentage }}"
|
||||
style="width: {{ transaction.percentage }}%">
|
||||
<span class="sr-only">{{ transaction.percentage }}% complete</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@ -19,13 +19,61 @@
|
||||
Main Plinth views
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from plinth import package as package_module
|
||||
from plinth.forms import PackageInstallForm
|
||||
|
||||
|
||||
def index(request):
|
||||
"""Serve the main index page"""
|
||||
"""Serve the main index page."""
|
||||
if request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('apps:index'))
|
||||
|
||||
return HttpResponseRedirect(reverse('help:about'))
|
||||
|
||||
|
||||
class PackageInstallView(FormView):
|
||||
"""View to prompt and install packages."""
|
||||
template_name = 'package_install.html'
|
||||
form_class = PackageInstallForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return the context data rendering the template."""
|
||||
context = super(PackageInstallView, self).get_context_data(**kwargs)
|
||||
if 'packages_names' not in context:
|
||||
context['package_names'] = self.kwargs.get('package_names', [])
|
||||
|
||||
# Package details must have been resolved before building the form
|
||||
context['packages'] = [package_module.packages_resolved[package_name]
|
||||
for package_name in context['package_names']]
|
||||
context['is_installing'] = \
|
||||
package_module.is_installing(context['package_names'])
|
||||
context['transactions'] = package_module.transactions
|
||||
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the initial data to be filled in the form."""
|
||||
initial = super(PackageInstallView, self).get_initial()
|
||||
try:
|
||||
initial['package_names'] = ','.join(self.kwargs['package_names'])
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured('Argument package_names must be '
|
||||
'provided to PackageInstallView')
|
||||
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Handle successful validation of the form.
|
||||
|
||||
Start the package installation and show this view again.
|
||||
"""
|
||||
package_names = form.cleaned_data['package_names'].split(',')
|
||||
package_module.start_install(package_names)
|
||||
|
||||
return self.render_to_response(
|
||||
self.get_context_data(package_names=package_names))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user