diff --git a/data/etc/apt/apt.conf.d/20freedombox b/data/etc/apt/apt.conf.d/20freedombox new file mode 100644 index 000000000..7f5c6dce2 --- /dev/null +++ b/data/etc/apt/apt.conf.d/20freedombox @@ -0,0 +1,8 @@ +// This configuration is installed by FreedomBox. +// +// When Apt's cache is updated (i.e. apt-cache update) notify FreedomBox service +// via it's D-Bus API. FreedomBox may then handle upgrade of some packages. +// +APT::Update::Post-Invoke-Success { + "/usr/bin/test -S /var/run/dbus/system_bus_socket && /usr/bin/gdbus call --system --dest org.freedombox.Service --object-path /org/freedombox/Service/PackageHandler --timeout 10 --method org.freedombox.Service.PackageHandler.CacheUpdated 2> /dev/null > /dev/null; /bin/echo > /dev/null"; +}; diff --git a/data/usr/share/dbus-1/system.d/org.freedombox.Service.conf b/data/usr/share/dbus-1/system.d/org.freedombox.Service.conf new file mode 100644 index 000000000..cb5215fa0 --- /dev/null +++ b/data/usr/share/dbus-1/system.d/org.freedombox.Service.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/debian/control b/debian/control index 9c3c782f3..2aa8473a0 100644 --- a/debian/control +++ b/debian/control @@ -70,6 +70,8 @@ Depends: gir1.2-nm-1.0, javascript-common, ldapscripts, +# For gdbus used to call hooks into service + libglib2.0-bin, libjs-bootstrap, libjs-jquery, libjs-modernizr, diff --git a/plinth/__main__.py b/plinth/__main__.py index d6b472c8a..0afdbead8 100644 --- a/plinth/__main__.py +++ b/plinth/__main__.py @@ -23,7 +23,7 @@ import sys import axes -from . import (cfg, frontpage, log, menu, module_loader, service, setup, +from . import (cfg, dbus, frontpage, log, menu, module_loader, service, setup, web_framework, web_server) axes.default_app_config = "plinth.axes_app_config.AppConfig" @@ -129,6 +129,7 @@ def adapt_config(arguments): def on_web_server_stop(): """Stop all other threads since web server is trying to exit.""" setup.stop() + dbus.stop() def main(): @@ -177,6 +178,8 @@ def main(): setup.run_setup_in_background() + dbus.run() + web_server.init() web_server.run(on_web_server_stop) diff --git a/plinth/dbus.py b/plinth/dbus.py new file mode 100755 index 000000000..c3e7a815b --- /dev/null +++ b/plinth/dbus.py @@ -0,0 +1,154 @@ +# +# This file is part of FreedomBox. +# +# 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 . +# +""" +Expose some API over D-Bus. +""" + +import logging +import threading + +from plinth.utils import import_from_gi + +from . import setup + +glib = import_from_gi('GLib', '2.0') +gio = import_from_gi('Gio', '2.0') + +logger = logging.getLogger(__name__) + +_thread = None +_server = None +_main_loop = None + + +class PackageHandler(): + """D-Bus service to listen for messages when apt cache is updated.""" + + introspection_xml = ''' + + + + + + + +''' + + def register(self, connection): + """Register the object in D-Bus connection.""" + introspection_data = gio.DBusNodeInfo.new_for_xml( + self.introspection_xml) + interface_info = gio.DBusNodeInfo.lookup_interface( + introspection_data, 'org.freedombox.Service.PackageHandler') + connection.register_object('/org/freedombox/Service/PackageHandler', + interface_info, self.on_method_call, None, + None) + + def on_method_call(self, _connection, _sender, _object_path, + _interface_name, method_name, _parameters, invocation): + """Handle method being called. + + No need to check all the incoming parameters as D-Bus will validate all + the incoming parameters using introspection data. + + """ + if method_name == 'CacheUpdated': + self.on_cache_updated() + invocation.return_value() + + @staticmethod + def on_cache_updated(): + """Called when system package cache is updated.""" + logger.info('Apt package cache updated.') + + # Run in a new thread because we don't want to block the thread running + # Glib main loop. + threading.Thread(target=setup.on_package_cache_updated).start() + + +class DBusServer(): + """Abstraction over a connection to D-Bus.""" + + def __init__(self): + """Initialize the server object.""" + self.package_handler = None + + def connect(self): + """Connect to bus with well-known name.""" + gio.bus_own_name(gio.BusType.SYSTEM, 'org.freedombox.Service', + gio.BusNameOwnerFlags.NONE, self.on_bus_acquired, + self.on_name_acquired, self.on_name_lost) + + def on_bus_acquired(self, connection, name): + """Callback when connection to D-Bus has been acquired.""" + logger.info('D-Bus connection acquired: %s', name) + self.package_handler = PackageHandler() + self.package_handler.register(connection) + + @staticmethod + def on_name_acquired(_connection, name): + """Callback when service name on D-Bus has been acquired.""" + logger.info('D-Bus name acquired: %s', name) + + @staticmethod + def on_name_lost(_connection, name): + """Callback when service name or DBus connection is closed.""" + logger.info('D-Bus connection lost: %s', name) + + # XXX: Reconnect after a while + # + # Such as by doing: + # connection.set_exit_on_close(False) + # gio.timeout_add(10000, self.connect) + # + # However, perhaps due to some cleanup issues, reconnection is not + # happening if it is does after an incoming method call. + # + # If D-Bus connection is lost due to daemon restart, FreedomBox service + # will receive a SIGTERM and exit. systemd should then restart the + # service again. + + +def run(): + """Run a glib main loop forever in a thread.""" + global _thread + _thread = threading.Thread(target=_run) + _thread.start() + + +def stop(): + """Exit glib main loop and end the thread.""" + if _main_loop: + logger.info('Exiting main loop for D-Bus services') + _main_loop.quit() + + +def _run(): + """Connect to D-Bus and run main loop.""" + logger.info('Started new thread for D-Bus services') + + global _server + _server = DBusServer() + _server.connect() + + global _main_loop + _main_loop = glib.MainLoop() + _main_loop.run() + _main_loop = None + + logger.info('D-Bus services thread exited.') diff --git a/plinth/setup.py b/plinth/setup.py index 05f3c9c3c..1f8894d70 100644 --- a/plinth/setup.py +++ b/plinth/setup.py @@ -37,6 +37,8 @@ _is_first_setup = False is_first_setup_running = False _is_shutting_down = False +_force_upgrader = None + class Helper(object): """Helper routines for modules to show progress.""" @@ -335,3 +337,31 @@ def run_setup_on_modules(module_list, allow_install=True): except Exception as exception: logger.error('Error running setup - %s', exception) raise + + +class ForceUpgrader(): + """Find and upgrade packages by force when conffile prompt is needed.""" + + def on_package_cache_updated(self): + """Find an upgrade packages.""" + packages = self.get_list_of_upgradeable_packages() + if packages: + logger.info('Packages available for upgrade: %s', + ', '.join([package.name for package in packages])) + + # XXX: Implement force upgrading of selected packages + + @staticmethod + def get_list_of_upgradeable_packages(): + """Return list of packages that can be upgraded.""" + cache = apt.cache.Cache() + return [package for package in cache if package.is_upgradable] + + +def on_package_cache_updated(): + """Called by D-Bus service when apt package cache is updated.""" + global _force_upgrader + if not _force_upgrader: + _force_upgrader = ForceUpgrader() + + _force_upgrader.on_package_cache_updated() diff --git a/setup.py b/setup.py index f55ff4bef..9c62b4665 100755 --- a/setup.py +++ b/setup.py @@ -236,10 +236,11 @@ setuptools.setup( glob.glob('data/etc/apache2/sites-available/*.conf')), ('/etc/apache2/includes', glob.glob('data/etc/apache2/includes/*.conf')), - ('/etc/apt/apt.conf.d', - glob.glob('data/etc/apt/apt.conf.d/60unattended-upgrades')), - ('/etc/avahi/services/', - glob.glob('data/etc/avahi/services/*.service')), + ('/etc/apt/apt.conf.d', [ + 'data/etc/apt/apt.conf.d/60unattended-upgrades', + 'data/etc/apt/apt.conf.d/20freedombox' + ]), ('/etc/avahi/services/', + glob.glob('data/etc/avahi/services/*.service')), ('/etc/ikiwiki', glob.glob('data/etc/ikiwiki/*.setup')), ('/etc/NetworkManager/dispatcher.d/', [ 'data/etc/NetworkManager/dispatcher.d/10-freedombox-batman' @@ -247,9 +248,10 @@ setuptools.setup( 'data/etc/sudoers.d/plinth' ]), ('/lib/systemd/system', glob.glob('data/lib/systemd/system/*.service')), - ('/lib/systemd/system/mldonkey-server.service.d', - ['data/lib/systemd/system/mldonkey-server.service.d/freedombox.conf']), - ('/lib/systemd/system', glob.glob('data/lib/systemd/system/*.timer')), + ('/lib/systemd/system/mldonkey-server.service.d', [ + 'data/lib/systemd/system/mldonkey-server.service.d/freedombox.conf' + ]), ('/lib/systemd/system', + glob.glob('data/lib/systemd/system/*.timer')), ('/etc/mediawiki', glob.glob('data/etc/mediawiki/*.php')), ('/etc/update-motd.d/', [ 'data/etc/update-motd.d/50-freedombox' @@ -265,6 +267,8 @@ setuptools.setup( glob.glob('data/usr/share/augeas/lenses/*.aug')), ('/usr/share/augeas/lenses/tests', glob.glob('data/usr/share/augeas/lenses/tests/test_*.aug')), + ('/usr/share/dbus-1/system.d', + glob.glob('data/usr/share/dbus-1/system.d/*.conf')), ('/usr/share/pam-configs/', glob.glob('data/usr/share/pam-configs/*-freedombox')), ('/etc/fail2ban/jail.d', glob.glob('data/etc/fail2ban/jail.d/*.conf')),