diff --git a/src/device.c b/src/device.c index defaee2..d505761 100644 --- a/src/device.c +++ b/src/device.c @@ -475,6 +475,81 @@ _fprint_device_get_id (FprintDevice *rdev) return priv->id; } +static void +suspend_cb (GObject *source_obj, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + + fp_device_suspend_finish (FP_DEVICE (source_obj), res, &error); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +static void +resume_cb (GObject *source_obj, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GTask) task = user_data; + GError *error = NULL; + + fp_device_resume_finish (FP_DEVICE (source_obj), res, &error); + if (error) + g_task_return_error (task, error); + else + g_task_return_boolean (task, TRUE); +} + +void +fprint_device_suspend (FprintDevice *rdev, + GAsyncReadyCallback callback, + void *user_data) +{ + GTask *task = NULL; + FprintDevicePrivate *priv = fprint_device_get_instance_private (rdev); + + /* Just forward to libfprint. */ + + task = g_task_new (rdev, NULL, callback, user_data); + fp_device_suspend (priv->dev, NULL, suspend_cb, task); +} + +void +fprint_device_resume (FprintDevice *rdev, + GAsyncReadyCallback callback, + void *user_data) +{ + GTask *task = NULL; + FprintDevicePrivate *priv = fprint_device_get_instance_private (rdev); + + /* Just forward to libfprint. */ + + task = g_task_new (rdev, NULL, callback, user_data); + fp_device_resume (priv->dev, NULL, resume_cb, task); +} + +void +fprint_device_suspend_finish (FprintDevice *rdev, + GAsyncResult *res, + GError **error) +{ + g_task_propagate_boolean (G_TASK (res), error); +} + +void +fprint_device_resume_finish (FprintDevice *rdev, + GAsyncResult *res, + GError **error) +{ + g_task_propagate_boolean (G_TASK (res), error); +} + + static const char * fp_finger_to_name (FpFinger finger) { diff --git a/src/fprintd.h b/src/fprintd.h index d6233a9..d414e68 100644 --- a/src/fprintd.h +++ b/src/fprintd.h @@ -95,6 +95,22 @@ struct _FprintDevice FprintDevice *fprint_device_new (FpDevice *dev); guint32 _fprint_device_get_id (FprintDevice *rdev); + +void fprint_device_suspend (FprintDevice *rdev, + GAsyncReadyCallback callback, + void *user_data); +void fprint_device_resume (FprintDevice *rdev, + GAsyncReadyCallback callback, + void *user_data); + +void fprint_device_suspend_finish (FprintDevice *rdev, + GAsyncResult *result, + GError **error); +void fprint_device_resume_finish (FprintDevice *rdev, + GAsyncResult *res, + GError **error); + + /* Print */ /* TODO */ diff --git a/src/manager.c b/src/manager.c index 38c5a54..a3dae5d 100644 --- a/src/manager.c +++ b/src/manager.c @@ -24,9 +24,14 @@ #include #include #include +#include #include "fprintd.h" +#define LOGIND_BUS_NAME "org.freedesktop.login1" +#define LOGIND_IFACE_NAME "org.freedesktop.login1.Manager" +#define LOGIND_OBJ_PATH "/org/freedesktop/login1" + static void fprint_manager_constructed (GObject *object); static gboolean fprint_manager_get_devices (FprintManager *manager, GPtrArray **devices, @@ -43,6 +48,9 @@ typedef struct FpContext *context; gboolean no_timeout; guint timeout_id; + gint prepare_for_sleep_pending; + guint prepare_for_sleep_id; + gint sleep_inhibit_fd; } FprintManagerPrivate; G_DEFINE_TYPE_WITH_CODE (FprintManager, fprint_manager, G_TYPE_OBJECT, G_ADD_PRIVATE (FprintManager)) @@ -60,6 +68,10 @@ fprint_manager_finalize (GObject *object) { FprintManagerPrivate *priv = fprint_manager_get_instance_private (FPRINT_MANAGER (object)); + if (priv->prepare_for_sleep_id) + g_dbus_connection_signal_unsubscribe (priv->connection, + priv->prepare_for_sleep_id); + g_clear_object (&priv->object_manager); g_clear_object (&priv->dbus_manager); g_clear_object (&priv->connection); @@ -227,6 +239,153 @@ handle_get_default_device (FprintManager *manager, return TRUE; } +static void +fprint_device_suspend_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + FprintManager *manager = FPRINT_MANAGER (user_data); + FprintManagerPrivate *priv = fprint_manager_get_instance_private (manager); + + /* Fetch the result (except for the NULL dummy call). */ + if (source_object != NULL) + { + fprint_device_suspend_finish (FPRINT_DEVICE (source_object), res, &error); + if (error) + { + if (!g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_OPEN) && + !g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_SUPPORTED)) + g_message ("Unexpected error while suspending device: %s", error->message); + } + } + + priv->prepare_for_sleep_pending -= 1; + + /* Close FD when all devices are prepared for sleeping. */ + if (priv->prepare_for_sleep_pending == 0) + { + if (priv->sleep_inhibit_fd >= 0) + close (priv->sleep_inhibit_fd); + priv->sleep_inhibit_fd = -1; + g_debug ("Released delay inhibitor for sleep."); + } + + g_object_unref (manager); +} + +static void +logind_sleep_inhibit_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(FprintManager) manager = FPRINT_MANAGER (user_data); + FprintManagerPrivate *priv = fprint_manager_get_instance_private (manager); + gint fd_offset; + + data = g_dbus_connection_call_with_unix_fd_list_finish (priv->connection, &out_fd_list, res, &error); + + if (!data) + { + g_warning ("Failed to install a sleep delay inhibitor: %s", error->message); + return; + } + + if (priv->sleep_inhibit_fd >= 0) + close (priv->sleep_inhibit_fd); + + g_debug ("Got delay inhibitor for sleep."); + + g_variant_get (data, "(h)", &fd_offset); + priv->sleep_inhibit_fd = g_unix_fd_list_get (out_fd_list, fd_offset, NULL); +} + +static void +handle_prepare_for_sleep_signal (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + g_autolist (GDBusObject) devices = NULL; + FprintManager *manager = FPRINT_MANAGER (user_data); + FprintManagerPrivate *priv = fprint_manager_get_instance_private (manager); + gboolean prepare_for_sleep; + GList *l; + + if (!g_variant_check_format_string (parameters, "(b)", FALSE)) + { + g_warning ("Received incorrect parameter for PrepareForSleep signal"); + return; + } + + g_variant_get (parameters, "(b)", &prepare_for_sleep); + + /* called one more time to handle the case of no devices */ + if (prepare_for_sleep) + priv->prepare_for_sleep_pending = 1; + + devices = g_dbus_object_manager_get_objects (priv->object_manager); + + g_debug ("Preparing devices for %s", prepare_for_sleep ? "sleep" : "resume"); + + for (l = devices; l != NULL; l = l->next) + { + g_autoptr(FprintDevice) dev = NULL; + FprintDBusObjectSkeleton *object = l->data; + + dev = fprint_dbus_object_skeleton_get_device (object); + + if (prepare_for_sleep) + { + priv->prepare_for_sleep_pending += 1; + g_object_ref (manager); + fprint_device_suspend (dev, fprint_device_suspend_cb, manager); + } + else + { + fprint_device_resume (dev, NULL, NULL); + } + } + + if (prepare_for_sleep) + { + /* "Notify" the initial dummy device we added, handling no devices that suspending */ + g_object_ref (manager); + fprint_device_suspend_cb (NULL, NULL, manager); + } + else + { + GVariant *arg = NULL; + + arg = g_variant_new ("(ssss)", + "sleep", + "net.reactivated.Fprint", + "Suspend fingerprint readers", + "delay"); + + /* Grab a sleep inhibitor. */ + g_dbus_connection_call_with_unix_fd_list (priv->connection, + LOGIND_BUS_NAME, + LOGIND_OBJ_PATH, + LOGIND_IFACE_NAME, + "Inhibit", + arg, + G_VARIANT_TYPE ("(h)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + logind_sleep_inhibit_cb, + g_object_ref (manager)); + } +} + static void device_added_cb (FprintManager *manager, FpDevice *device, FpContext *context) { @@ -290,6 +449,7 @@ device_removed_cb (FprintManager *manager, FpDevice *device, FpContext *context) static void fprint_manager_constructed (GObject *object) { + g_autoptr(GVariant) param_false = NULL; FprintManager *manager = FPRINT_MANAGER (object); FprintManagerPrivate *priv = fprint_manager_get_instance_private (manager); GDBusObjectManagerServer *object_manager_server; @@ -319,6 +479,20 @@ fprint_manager_constructed (GObject *object) g_dbus_object_manager_server_set_connection (object_manager_server, priv->connection); + priv->prepare_for_sleep_id = g_dbus_connection_signal_subscribe (priv->connection, + LOGIND_BUS_NAME, + LOGIND_IFACE_NAME, + "PrepareForSleep", + LOGIND_OBJ_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + handle_prepare_for_sleep_signal, + manager, + NULL); + /* Fake a resume as that triggers the inhibitor to be taken. */ + param_false = g_variant_new ("(b)", FALSE); + handle_prepare_for_sleep_signal (priv->connection, NULL, NULL, NULL, NULL, param_false, manager); + /* And register the signals for initial enumeration and hotplug. */ g_signal_connect_object (priv->context, "device-added", diff --git a/tests/fprintd.py b/tests/fprintd.py index 7936891..657fea6 100644 --- a/tests/fprintd.py +++ b/tests/fprintd.py @@ -247,10 +247,15 @@ class FPrintdTest(dbusmock.DBusTestCase): argv.insert(2, '--suppressions=%s' % valgrind) self.valgrind = True self.kill_daemon = False + self.daemon_log = OutputChecker() self.daemon = subprocess.Popen(argv, env=env, - stdout=None, + stdout=self.daemon_log.fd, stderr=subprocess.STDOUT) + self.daemon_log.writer_attached() + + #subprocess.Popen(['/usr/bin/dbus-monitor', '--system']) + self.addCleanup(self.daemon_stop) timeout_count = timeout * 10 @@ -310,6 +315,8 @@ class FPrintdTest(dbusmock.DBusTestCase): else: raise(e) + self.daemon_log.assert_closed() + if not self.kill_daemon: self.assertLess(self.daemon.returncode, 128) self.assertGreaterEqual(self.daemon.returncode, 0) @@ -591,8 +598,22 @@ class FPrintdVirtualDeviceBaseTest(FPrintdVirtualImageDeviceBaseTests): self.manager = None self.device = None self.polkitd_start() + + fifo_path = os.path.join(self.tmpdir, 'logind_inhibit_fifo') + os.mkfifo(fifo_path) + self.logind_inhibit_fifo = os.open(fifo_path, os.O_RDONLY | os.O_NONBLOCK | os.O_CLOEXEC) + # EOF without a writer, BlockingIOError with a writer + self.assertFalse(self.holds_inhibitor()) + + self.logind, self.logind_obj = self.spawn_server_template('logind', { }) + self.logind_obj.AddMethod('org.freedesktop.login1.Manager', 'Inhibit', 'ssss', 'h', + 'ret = os.open("%s", os.O_WRONLY)\n' % fifo_path + + 'from gi.repository import GLib\n' + + 'GLib.idle_add(lambda fd: os.close(fd), ret)') self.daemon_start(self.driver_name) + self.wait_got_delay_inhibitor() + if self.device is None: self.skipTest("Need {} device to run the test".format(self.device_driver)) @@ -641,6 +662,8 @@ class FPrintdVirtualDeviceBaseTest(FPrintdVirtualImageDeviceBaseTests): self.device = None self.manager = None + os.close(self.logind_inhibit_fifo) + super().tearDown() def try_release(self): @@ -672,6 +695,23 @@ class FPrintdVirtualDeviceBaseTest(FPrintdVirtualImageDeviceBaseTests): if expected is not None: self.assertEqual(self._last_result, expected) + def holds_inhibitor(self): + try: + if os.read(self.logind_inhibit_fifo, 1) == b'': + return False + except BlockingIOError: + return True + + raise AssertionError("logind inhibitor fifo in unexpected state") + + def wait_got_delay_inhibitor(self, timeout=0): + self.daemon_log.check_line('Got delay inhibitor for sleep', timeout=timeout) + self.assertTrue(self.holds_inhibitor()) + + def wait_released_delay_inhibitor(self, timeout=0): + self.daemon_log.check_line('Released delay inhibitor for sleep', timeout=timeout) + self.assertFalse(self.holds_inhibitor()) + def enroll_image(self, img, device=None, finger='right-index-finger', expected_result='enroll-completed', claim_user=None, start=True, stop=True): @@ -1538,6 +1578,72 @@ class FPrintdVirtualDeviceTest(FPrintdVirtualDeviceBaseTest): self.assertIn(GLib.Variant('()', ()), self.get_all_async_replies()) + def test_suspend_inhibit_unclaimed(self): + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [True]) + + self.daemon_log.check_line('Preparing devices for sleep', timeout=1) + self.wait_released_delay_inhibitor(timeout=1) + + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [False]) + + self.daemon_log.check_line('Preparing devices for resume', timeout=1) + self.wait_got_delay_inhibitor(timeout=1) + + def test_suspend_inhibit_claimed(self): + self.device.Claim('(s)', 'testuser') + + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [True]) + + self.daemon_log.check_line('Preparing devices for sleep', timeout=1) + self.wait_released_delay_inhibitor(timeout=1) + + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [False]) + + self.daemon_log.check_line('Preparing devices for resume', timeout=1) + self.wait_got_delay_inhibitor(timeout=1) + + self.device.Release() + + def test_suspend_inhibit_cancels_enroll(self): + self.device.Claim('(s)', 'testuser') + + self.device.EnrollStart('(s)', 'right-thumb') + + # Now prepare for sleep, which will trigger an internal cancellation + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [True]) + + self.daemon_log.check_line('Preparing devices for sleep', timeout=1) + self.wait_for_result(expected='enroll-unknown-error') + self.wait_released_delay_inhibitor(timeout=1) + + self.assertEqual(os.read(self.logind_inhibit_fifo, 1), b'') + + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [False]) + + self.daemon_log.check_line('Preparing devices for resume', timeout=1) + self.wait_got_delay_inhibitor(timeout=1) + + self.device.Release() + + def test_suspend_prevents_enroll(self): + self.device.Claim('(s)', 'testuser') + + # Now prepare for sleep, which will trigger an internal cancellation + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [True]) + + self.daemon_log.check_line('Preparing devices for sleep', timeout=1) + self.wait_released_delay_inhibitor(timeout=1) + + self.device.EnrollStart('(s)', 'right-thumb') + self.wait_for_result(expected='enroll-unknown-error') + + self.logind_obj.EmitSignal("", "PrepareForSleep", "b", [False]) + + self.daemon_log.check_line('Preparing devices for resume', timeout=1) + self.wait_got_delay_inhibitor(timeout=1) + + self.device.Release() + class FPrintdVirtualDeviceStorageTest(FPrintdVirtualStorageDeviceBaseTest, FPrintdVirtualDeviceTest):