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')),