diff --git a/libfprint/fp-device-private.h b/libfprint/fp-device-private.h index 9255076..99eba41 100644 --- a/libfprint/fp-device-private.h +++ b/libfprint/fp-device-private.h @@ -53,6 +53,7 @@ typedef struct gboolean is_removed; gboolean is_open; + gboolean is_suspended; gchar *device_id; gchar *device_name; @@ -83,6 +84,12 @@ typedef struct guint critical_section; GSource *critical_section_flush_source; gboolean cancel_queued; + gboolean suspend_queued; + gboolean resume_queued; + + /* Suspend/resume tasks */ + GTask *suspend_resume_task; + GError *suspend_error; /* Device temperature model information and state */ GSource *temp_timeout; @@ -123,5 +130,7 @@ typedef struct void match_data_free (FpMatchData *match_data); +void fpi_device_configure_wakeup (FpDevice *device, + gboolean enabled); void fpi_device_update_temp (FpDevice *device, gboolean is_active); diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c index 1675864..209c418 100644 --- a/libfprint/fp-device.c +++ b/libfprint/fp-device.c @@ -339,6 +339,24 @@ fp_device_set_property (GObject *object, } } +static void +device_idle_probe_cb (FpDevice *self, gpointer user_data) +{ + /* This should not be an idle handler, see comment where it is registered. + * + * This effectively disables USB "persist" for us, and possibly turns off + * USB wakeup if it was enabled for some reason. + */ + fpi_device_configure_wakeup (self, FALSE); + + if (!FP_DEVICE_GET_CLASS (self)->probe) + fpi_device_probe_complete (self, NULL, NULL, NULL); + else + FP_DEVICE_GET_CLASS (self)->probe (self); + + return; +} + static void fp_device_async_initable_init_async (GAsyncInitable *initable, int io_priority, @@ -358,17 +376,16 @@ fp_device_async_initable_init_async (GAsyncInitable *initable, if (g_task_return_error_if_cancelled (task)) return; - if (!FP_DEVICE_GET_CLASS (self)->probe) - { - g_task_return_boolean (task, TRUE); - return; - } - priv->current_action = FPI_DEVICE_ACTION_PROBE; priv->current_task = g_steal_pointer (&task); setup_task_cancellable (self); - FP_DEVICE_GET_CLASS (self)->probe (self); + /* We push this into an idle handler for compatibility with libgusb + * 0.3.7 and before. + * See https://github.com/hughsie/libgusb/pull/50 + */ + g_source_set_name (fpi_device_add_timeout (self, 0, device_idle_probe_cb, NULL, NULL), + "libusb probe in idle"); } static gboolean @@ -794,7 +811,7 @@ fp_device_open (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -879,7 +896,7 @@ fp_device_close (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -912,6 +929,230 @@ fp_device_close_finish (FpDevice *device, return g_task_propagate_boolean (G_TASK (result), error); } +static void +complete_suspend_resume_task (FpDevice *device) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + g_assert (priv->suspend_resume_task); + + g_task_return_boolean (g_steal_pointer (&priv->suspend_resume_task), TRUE); +} + +/** + * fp_device_suspend: + * @device: a #FpDevice + * @cancellable: (nullable): a #GCancellable, or %NULL, currently not used + * @callback: the function to call on completion + * @user_data: the data to pass to @callback + * + * Prepare the device for system suspend. Retrieve the result with + * fp_device_suspend_finish(). + * + * The suspend method can be called at any time (even if the device is not + * opened) and must be paired with a corresponding resume call. It is undefined + * when or how any ongoing operation is finished. This call might wait for an + * ongoing operation to finish, might cancel the ongoing operation or may + * prepare the device so that the host is resumed when the operation can be + * finished. + * + * If an ongoing operation must be cancelled then it will complete with an error + * code of #FP_DEVICE_ERROR_BUSY before the suspend async routine finishes. + * + * Any operation started while the device is suspended will fail with + * #FP_DEVICE_ERROR_BUSY, this includes calls to open or close the device. + */ +void +fp_device_suspend (FpDevice *device, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + task = g_task_new (device, cancellable, callback, user_data); + + if (priv->suspend_resume_task || priv->is_suspended) + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); + return; + } + + if (priv->is_removed) + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_REMOVED)); + return; + } + + priv->suspend_resume_task = g_steal_pointer (&task); + + /* If the device is currently idle, just complete immediately. + * For long running tasks, call the driver handler right away, for short + * tasks, wait for completion and then return the task. + */ + switch (priv->current_action) + { + case FPI_DEVICE_ACTION_NONE: + fpi_device_suspend_complete (device, NULL); + break; + + case FPI_DEVICE_ACTION_ENROLL: + case FPI_DEVICE_ACTION_VERIFY: + case FPI_DEVICE_ACTION_IDENTIFY: + case FPI_DEVICE_ACTION_CAPTURE: + if (FP_DEVICE_GET_CLASS (device)->suspend) + { + if (priv->critical_section) + priv->suspend_queued = TRUE; + else + FP_DEVICE_GET_CLASS (device)->suspend (device); + } + else + { + fpi_device_suspend_complete (device, fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED)); + } + break; + + default: + case FPI_DEVICE_ACTION_PROBE: + case FPI_DEVICE_ACTION_OPEN: + case FPI_DEVICE_ACTION_CLOSE: + case FPI_DEVICE_ACTION_DELETE: + case FPI_DEVICE_ACTION_LIST: + case FPI_DEVICE_ACTION_CLEAR_STORAGE: + g_signal_connect_object (priv->current_task, + "notify::completed", + G_CALLBACK (complete_suspend_resume_task), + device, + G_CONNECT_SWAPPED); + + break; + } +} + +/** + * fp_device_suspend_finish: + * @device: A #FpDevice + * @result: A #GAsyncResult + * @error: Return location for errors, or %NULL to ignore + * + * Finish an asynchronous operation to prepare the device for suspend. + * See fp_device_suspend(). + * + * The API user should accept an error of #FP_DEVICE_ERROR_NOT_SUPPORTED. + * + * Returns: (type void): %FALSE on error, %TRUE otherwise + */ +gboolean +fp_device_suspend_finish (FpDevice *device, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +/** + * fp_device_resume: + * @device: a #FpDevice + * @cancellable: (nullable): a #GCancellable, or %NULL, currently not used + * @callback: the function to call on completion + * @user_data: the data to pass to @callback + * + * Resume device after system suspend. Retrieve the result with + * fp_device_suspend_finish(). + * + * Note that it is not defined when any ongoing operation may return (success or + * error). You must be ready to handle this before, during or after the + * resume operation. + */ +void +fp_device_resume (FpDevice *device, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + task = g_task_new (device, cancellable, callback, user_data); + + if (priv->suspend_resume_task || !priv->is_suspended) + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); + return; + } + + if (priv->is_removed) + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_REMOVED)); + return; + } + + priv->suspend_resume_task = g_steal_pointer (&task); + + switch (priv->current_action) + { + case FPI_DEVICE_ACTION_NONE: + fpi_device_resume_complete (device, NULL); + break; + + case FPI_DEVICE_ACTION_ENROLL: + case FPI_DEVICE_ACTION_VERIFY: + case FPI_DEVICE_ACTION_IDENTIFY: + case FPI_DEVICE_ACTION_CAPTURE: + if (FP_DEVICE_GET_CLASS (device)->resume) + { + if (priv->critical_section) + priv->resume_queued = TRUE; + else + FP_DEVICE_GET_CLASS (device)->resume (device); + } + else + { + fpi_device_resume_complete (device, fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED)); + } + break; + + default: + case FPI_DEVICE_ACTION_PROBE: + case FPI_DEVICE_ACTION_OPEN: + case FPI_DEVICE_ACTION_CLOSE: + case FPI_DEVICE_ACTION_DELETE: + case FPI_DEVICE_ACTION_LIST: + case FPI_DEVICE_ACTION_CLEAR_STORAGE: + /* cannot happen as we make sure these tasks complete before suspend */ + g_assert_not_reached(); + complete_suspend_resume_task (device); + break; + } +} + +/** + * fp_device_resume_finish: + * @device: A #FpDevice + * @result: A #GAsyncResult + * @error: Return location for errors, or %NULL to ignore + * + * Finish an asynchronous operation to resume the device after suspend. + * See fp_device_resume(). + * + * The API user should accept an error of #FP_DEVICE_ERROR_NOT_SUPPORTED. + * + * Returns: (type void): %FALSE on error, %TRUE otherwise + */ +gboolean +fp_device_resume_finish (FpDevice *device, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + /** * fp_device_enroll: @@ -960,7 +1201,7 @@ fp_device_enroll (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1070,7 +1311,7 @@ fp_device_verify (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1197,7 +1438,7 @@ fp_device_identify (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1322,7 +1563,7 @@ fp_device_capture (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1413,7 +1654,7 @@ fp_device_delete_print (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1491,7 +1732,7 @@ fp_device_list_prints (FpDevice *device, return; } - if (priv->current_task) + if (priv->current_task || priv->is_suspended) { g_task_return_error (task, fpi_device_error_new (FP_DEVICE_ERROR_BUSY)); @@ -1887,6 +2128,7 @@ fp_device_list_prints_sync (FpDevice *device, return fp_device_list_prints_finish (device, task, error); } + /** * fp_device_clear_storage_sync: * @device: a #FpDevice @@ -1915,6 +2157,58 @@ fp_device_clear_storage_sync (FpDevice *device, return fp_device_clear_storage_finish (device, task, error); } +/** + * fp_device_suspend_sync: + * @device: a #FpDevice + * @cancellable: (nullable): a #GCancellable, or %NULL, currently not used + * @error: Return location for errors, or %NULL to ignore + * + * Prepare device for suspend. + * + * Returns: (type void): %FALSE on error, %TRUE otherwise + */ +gboolean +fp_device_suspend_sync (FpDevice *device, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GAsyncResult) task = NULL; + + g_return_val_if_fail (FP_IS_DEVICE (device), FALSE); + + fp_device_suspend (device, cancellable, async_result_ready, &task); + while (!task) + g_main_context_iteration (NULL, TRUE); + + return fp_device_suspend_finish (device, task, error); +} + +/** + * fp_device_resume_sync: + * @device: a #FpDevice + * @cancellable: (nullable): a #GCancellable, or %NULL, currently not used + * @error: Return location for errors, or %NULL to ignore + * + * Resume device after suspend. + * + * Returns: (type void): %FALSE on error, %TRUE otherwise + */ +gboolean +fp_device_resume_sync (FpDevice *device, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GAsyncResult) task = NULL; + + g_return_val_if_fail (FP_IS_DEVICE (device), FALSE); + + fp_device_resume (device, cancellable, async_result_ready, &task); + while (!task) + g_main_context_iteration (NULL, TRUE); + + return fp_device_resume_finish (device, task, error); +} + /** * fp_device_get_features: * @device: a #FpDevice diff --git a/libfprint/fp-device.h b/libfprint/fp-device.h index c920b00..85be34c 100644 --- a/libfprint/fp-device.h +++ b/libfprint/fp-device.h @@ -239,6 +239,16 @@ void fp_device_close (FpDevice *device, GAsyncReadyCallback callback, gpointer user_data); +void fp_device_suspend (FpDevice *device, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +void fp_device_resume (FpDevice *device, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + void fp_device_enroll (FpDevice *device, FpPrint *template_print, GCancellable *cancellable, @@ -294,6 +304,12 @@ gboolean fp_device_open_finish (FpDevice *device, gboolean fp_device_close_finish (FpDevice *device, GAsyncResult *result, GError **error); +gboolean fp_device_suspend_finish (FpDevice *device, + GAsyncResult *result, + GError **error); +gboolean fp_device_resume_finish (FpDevice *device, + GAsyncResult *result, + GError **error); FpPrint *fp_device_enroll_finish (FpDevice *device, GAsyncResult *result, GError **error); @@ -362,6 +378,13 @@ GPtrArray * fp_device_list_prints_sync (FpDevice *device, gboolean fp_device_clear_storage_sync (FpDevice *device, GCancellable *cancellable, GError **error); +gboolean fp_device_suspend_sync (FpDevice *device, + GCancellable *cancellable, + GError **error); +gboolean fp_device_resume_sync (FpDevice *device, + GCancellable *cancellable, + GError **error); + /* Deprecated functions */ G_DEPRECATED_FOR (fp_device_get_features) gboolean fp_device_supports_identify (FpDevice *device); diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c index 7aabd3f..89504da 100644 --- a/libfprint/fpi-device.c +++ b/libfprint/fpi-device.c @@ -20,6 +20,7 @@ #define FP_COMPONENT "device" #include +#include #include "fpi-log.h" @@ -863,6 +864,22 @@ fpi_device_critical_section_flush_idle_cb (FpDevice *device) return G_SOURCE_CONTINUE; } + if (priv->suspend_queued) + { + cls->suspend (device); + priv->suspend_queued = FALSE; + + return G_SOURCE_CONTINUE; + } + + if (priv->resume_queued) + { + cls->resume (device); + priv->resume_queued = FALSE; + + return G_SOURCE_CONTINUE; + } + priv->critical_section_flush_source = NULL; return G_SOURCE_REMOVE; @@ -998,15 +1015,6 @@ fp_device_task_return_in_idle_cb (gpointer user_data) return G_SOURCE_REMOVE; } - /* Return internal cancellation reason if we have one. - * Note that an external cancellation always returns G_IO_ERROR_CANCELLED */ - if (cancellation_reason) - { - g_task_return_error (task, g_steal_pointer (&cancellation_reason)); - - return G_SOURCE_REMOVE; - } - switch (data->type) { case FP_DEVICE_TASK_RETURN_INT: @@ -1028,7 +1036,18 @@ fp_device_task_return_in_idle_cb (gpointer user_data) break; case FP_DEVICE_TASK_RETURN_ERROR: - g_task_return_error (task, g_steal_pointer (&data->result)); + /* Return internal cancellation reason instead if we have one. + * Note that an external cancellation always returns G_IO_ERROR_CANCELLED + */ + if (cancellation_reason) + { + g_task_set_task_data (task, NULL, NULL); + g_task_return_error (task, g_steal_pointer (&cancellation_reason)); + } + else + { + g_task_return_error (task, g_steal_pointer (&data->result)); + } break; default: @@ -1531,6 +1550,183 @@ fpi_device_list_complete (FpDevice *device, fpi_device_return_task_in_idle (device, FP_DEVICE_TASK_RETURN_ERROR, error); } +void +fpi_device_configure_wakeup (FpDevice *device, gboolean enabled) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + switch (priv->type) + { + case FP_DEVICE_TYPE_USB: + { + g_autoptr(GString) ports = NULL; + GUsbDevice *dev, *parent; + const char *wakeup_command = enabled ? "enabled" : "disabled"; + guint8 bus, port; + g_autofree gchar *sysfs_wakeup = NULL; + g_autofree gchar *sysfs_persist = NULL; + gssize r; + int fd; + + ports = g_string_new (NULL); + bus = g_usb_device_get_bus (priv->usb_device); + + /* Walk up, skipping the root hub. */ + dev = priv->usb_device; + while ((parent = g_usb_device_get_parent (dev))) + { + port = g_usb_device_get_port_number (dev); + g_string_prepend (ports, g_strdup_printf ("%d.", port)); + dev = parent; + } + g_string_set_size (ports, ports->len - 1); + + sysfs_wakeup = g_strdup_printf ("/sys/bus/usb/devices/%d-%s/power/wakeup", bus, ports->str); + fd = open (sysfs_wakeup, O_WRONLY); + + if (fd < 0) + { + /* Wakeup not existing appears to be relatively normal. */ + g_debug ("Failed to open %s", sysfs_wakeup); + } + else + { + r = write (fd, wakeup_command, strlen (wakeup_command)); + if (r < 0) + g_warning ("Could not configure wakeup to %s by writing %s", wakeup_command, sysfs_wakeup); + close (fd); + } + + /* Persist means that the kernel tries to keep the USB device open + * in case it is "replugged" due to suspend. + * This is not helpful, as it will receive a reset and will be in a bad + * state. Instead, seeing an unplug and a new device makes more sense. + */ + sysfs_persist = g_strdup_printf ("/sys/bus/usb/devices/%d-%s/power/persist", bus, ports->str); + fd = open (sysfs_persist, O_WRONLY); + + if (fd < 0) + { + g_warning ("Failed to open %s", sysfs_persist); + return; + } + else + { + r = write (fd, "0", 1); + if (r < 0) + g_message ("Could not disable USB persist by writing to %s", sysfs_persist); + close (fd); + } + + break; + } + + case FP_DEVICE_TYPE_VIRTUAL: + case FP_DEVICE_TYPE_UDEV: + break; + + default: + g_assert_not_reached (); + fpi_device_return_task_in_idle (device, FP_DEVICE_TASK_RETURN_ERROR, + fpi_device_error_new (FP_DEVICE_ERROR_GENERAL)); + return; + } +} + +static void +fpi_device_suspend_completed (FpDevice *device) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + /* We have an ongoing operation, allow the device to wake up the machine. */ + if (priv->current_action != FPI_DEVICE_ACTION_NONE) + fpi_device_configure_wakeup (device, TRUE); + + if (priv->critical_section) + g_warning ("Driver was in a critical section at suspend time. It likely deadlocked!"); + + if (priv->suspend_error) + g_task_return_error (g_steal_pointer (&priv->suspend_resume_task), + g_steal_pointer (&priv->suspend_error)); + else + g_task_return_boolean (g_steal_pointer (&priv->suspend_resume_task), TRUE); +} + +/** + * fpi_device_suspend_complete: + * @device: The #FpDevice + * @error: The #GError or %NULL on success + * + * Finish a suspend request. Only return a %NULL error if suspend has been + * correctly configured and the current action as returned by + * fpi_device_get_current_action() will continue to run after resume. + * + * In all other cases an error must be returned. Should this happen, the + * current action will be cancelled before the error is forwarded to the + * application. + * + * It is recommended to set @error to #FP_ERROR_NOT_IMPLEMENTED. + */ +void +fpi_device_suspend_complete (FpDevice *device, + GError *error) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + g_return_if_fail (FP_IS_DEVICE (device)); + g_return_if_fail (priv->suspend_resume_task); + g_return_if_fail (priv->suspend_error == NULL); + + priv->suspend_error = error; + priv->is_suspended = TRUE; + + /* If there is no error, we have no running task, return immediately. */ + if (error == NULL || !priv->current_task || g_task_get_completed (priv->current_task)) + { + fpi_device_suspend_completed (device); + return; + } + + /* Wait for completion of the current task. */ + g_signal_connect_object (priv->current_task, + "notify::completed", + G_CALLBACK (fpi_device_suspend_completed), + device, + G_CONNECT_SWAPPED); + + /* And cancel any action that might be long-running. */ + if (!priv->current_cancellation_reason) + priv->current_cancellation_reason = fpi_device_error_new_msg (FP_DEVICE_ERROR_BUSY, + "Cannot run while suspended."); + + g_cancellable_cancel (priv->current_cancellable); +} + +/** + * fpi_device_resume_complete: + * @device: The #FpDevice + * @error: The #GError or %NULL on success + * + * Finish a resume request. + */ +void +fpi_device_resume_complete (FpDevice *device, + GError *error) +{ + FpDevicePrivate *priv = fp_device_get_instance_private (device); + + g_return_if_fail (FP_IS_DEVICE (device)); + g_return_if_fail (priv->suspend_resume_task); + + priv->is_suspended = FALSE; + fpi_device_configure_wakeup (device, FALSE); + + if (error) + g_task_return_error (g_steal_pointer (&priv->suspend_resume_task), error); + else + g_task_return_boolean (g_steal_pointer (&priv->suspend_resume_task), TRUE); +} + /** * fpi_device_clear_storage_complete: * @device: The #FpDevice diff --git a/libfprint/fpi-device.h b/libfprint/fpi-device.h index 6519a55..42e26e0 100644 --- a/libfprint/fpi-device.h +++ b/libfprint/fpi-device.h @@ -108,6 +108,10 @@ struct _FpIdEntry * @clear_storage: Delete all prints from the device * @cancel: Called on cancellation, this is a convenience to not need to handle * the #GCancellable directly by using fpi_device_get_cancellable(). + * @suspend: Called when an interactive action is running (ENROLL, VERIFY, + * IDENTIFY or CAPTURE) and the system is about to go into suspend. + * @resume: Called to resume an ongoing interactive action after the system has + * resumed from suspend. * * NOTE: If your driver is image based, then you should subclass #FpImageDevice * instead. #FpImageDevice based drivers use a different way of interacting @@ -126,6 +130,9 @@ struct _FpIdEntry * operation (i.e. any operation that requires capturing). It is entirely fine * to ignore cancellation requests for short operations (e.g. open/close). * + * Note that @cancel, @suspend and @resume will not be called while the device + * is within a fpi_device_critical_enter()/fpi_device_critical_leave() block. + * * This API is solely intended for drivers. It is purely internal and neither * API nor ABI stable. */ @@ -164,6 +171,8 @@ struct _FpDeviceClass void (*clear_storage) (FpDevice * device); void (*cancel) (FpDevice *device); + void (*suspend) (FpDevice *device); + void (*resume) (FpDevice *device); }; void fpi_device_class_auto_initialize_features (FpDeviceClass *device_class); @@ -292,6 +301,10 @@ void fpi_device_list_complete (FpDevice *device, GError *error); void fpi_device_clear_storage_complete (FpDevice *device, GError *error); +void fpi_device_suspend_complete (FpDevice *device, + GError *error); +void fpi_device_resume_complete (FpDevice *device, + GError *error); void fpi_device_enroll_progress (FpDevice *device, gint completed_stages,