Previous six patches are related to this change. This patch moves: * GObject boilerplate * External API related to SpiceFileTransferTask * Internal API needed by channel-main * Helpers that belong to this object --- src/Makefile.am | 2 + src/channel-main.c | 682 +---------------------------------- src/spice-file-transfer-task-priv.h | 56 +++ src/spice-file-transfer-task.c | 697 ++++++++++++++++++++++++++++++++++++ 4 files changed, 756 insertions(+), 681 deletions(-) create mode 100644 src/spice-file-transfer-task-priv.h create mode 100644 src/spice-file-transfer-task.c diff --git a/src/Makefile.am b/src/Makefile.am index 73bb39c..35ac906 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -233,6 +233,8 @@ libspice_client_glib_2_0_la_SOURCES = \ spice-channel.c \ spice-channel-cache.h \ spice-channel-priv.h \ + spice-file-transfer-task.c \ + spice-file-transfer-task-priv.h \ coroutine.h \ gio-coroutine.c \ gio-coroutine.h \ diff --git a/src/channel-main.c b/src/channel-main.c index efe559c..2fcc382 100644 --- a/src/channel-main.c +++ b/src/channel-main.c @@ -29,7 +29,7 @@ #include "spice-channel-priv.h" #include "spice-session-priv.h" #include "spice-audio-priv.h" -#include "spice-file-transfer-task.h" +#include "spice-file-transfer-task-priv.h" /** * SECTION:channel-main @@ -54,87 +54,6 @@ typedef struct spice_migrate spice_migrate; -/** - * SECTION:file-transfer-task - * @short_description: Monitoring file transfers - * @title: File Transfer Task - * @section_id: - * @see_also: #SpiceMainChannel - * @stability: Stable - * @include: spice-client.h - * - * SpiceFileTransferTask is an object that represents a particular file - * transfer between the client and the guest. The properties and signals of the - * object can be used to monitor the status and result of the transfer. The - * Main Channel's #SpiceMainChannel::new-file-transfer signal will be emitted - * whenever a new file transfer task is initiated. - * - * Since: 0.31 - */ -G_DEFINE_TYPE(SpiceFileTransferTask, spice_file_transfer_task, G_TYPE_OBJECT) - -#define FILE_TRANSFER_TASK_PRIVATE(o) \ - (G_TYPE_INSTANCE_GET_PRIVATE((o), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskPrivate)) - -#define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32) - -typedef void (*SpiceFileTransferTaskFlushCb)(SpiceFileTransferTask *xfer_task, - void *buffer, - gssize count, - gpointer user_data); - -static SpiceMainChannel *spice_file_transfer_task_get_channel(SpiceFileTransferTask *self); -static GCancellable *spice_file_transfer_task_get_cancellable(SpiceFileTransferTask *self); -static void spice_file_transfer_task_flush_done(SpiceFileTransferTask *self, GError *error); -static GList *spice_file_transfer_task_create_tasks(SpiceMainChannel *channel, - GFile **files, - GFileCopyFlags flags, - GCancellable *cancellable, - SpiceFileTransferTaskFlushCb flush_callback, - gpointer flush_callback_data, - GAsyncReadyCallback callback, - gpointer user_data); -static void spice_file_transfer_task_start_task(SpiceFileTransferTask *self); - -struct _SpiceFileTransferTaskPrivate - -/* private */ -{ - uint32_t id; - gboolean pending; - GFile *file; - SpiceMainChannel *channel; - GFileInputStream *file_stream; - GFileCopyFlags flags; - GCancellable *cancellable; - SpiceFileTransferTaskFlushCb flush_callback; - gpointer flush_callback_data; - GAsyncReadyCallback callback; - gpointer user_data; - char *buffer; - uint64_t read_bytes; - uint64_t file_size; - gint64 start_time; - gint64 last_update; - GError *error; -}; - -enum { - PROP_TASK_ID = 1, - PROP_TASK_CHANNEL, - PROP_TASK_CANCELLABLE, - PROP_TASK_FILE, - PROP_TASK_PROGRESS, -}; - -enum { - SIGNAL_FINISHED, - SIGNAL_FILE_INFO, - LAST_TASK_SIGNAL -}; - -static guint task_signals[LAST_TASK_SIGNAL]; - typedef enum { DISPLAY_UNDEFINED, DISPLAY_DISABLED, @@ -253,8 +172,6 @@ static void migrate_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent ev gpointer data); static gboolean main_migrate_handshake_done(gpointer data); static void spice_main_channel_send_migration_handshake(SpiceChannel *channel); -static void file_xfer_continue_read(SpiceFileTransferTask *task); -static void spice_file_transfer_task_completed(SpiceFileTransferTask *self, GError *error); static void file_xfer_flushed(SpiceMainChannel *channel, gboolean success); static void spice_main_set_max_clipboard(SpiceMainChannel *self, gint max); static void set_agent_connected(SpiceMainChannel *channel, gboolean connected); @@ -1814,59 +1731,6 @@ static void main_handle_agent_disconnected(SpiceChannel *channel, SpiceMsgIn *in agent_stopped(SPICE_MAIN_CHANNEL(channel)); } -/* main context */ -static void file_xfer_close_cb(GObject *object, - GAsyncResult *close_res, - gpointer user_data) -{ - GTask *task; - SpiceFileTransferTask *self; - GError *error = NULL; - - self = user_data; - - if (object) { - GInputStream *stream = G_INPUT_STREAM(object); - g_input_stream_close_finish(stream, close_res, &error); - if (error) { - /* This error dont need to report to user, just print a log */ - SPICE_DEBUG("close file error: %s", error->message); - g_clear_error(&error); - } - } - - /* Notify to user that files have been transferred or something error - happened. */ - task = g_task_new(self->priv->channel, - self->priv->cancellable, - self->priv->callback, - self->priv->user_data); - - if (self->priv->error) { - g_task_return_error(task, self->priv->error); - } else { - g_task_return_boolean(task, TRUE); - if (spice_util_get_debug()) { - gint64 now = g_get_monotonic_time(); - gchar *basename = g_file_get_basename(self->priv->file); - double seconds = (double) (now - self->priv->start_time) / G_TIME_SPAN_SECOND; - gchar *file_size_str = g_format_size(self->priv->file_size); - gchar *transfer_speed_str = g_format_size(self->priv->file_size / seconds); - - g_warn_if_fail(self->priv->read_bytes == self->priv->file_size); - SPICE_DEBUG("transferred file %s of %s size in %.1f seconds (%s/s)", - basename, file_size_str, seconds, transfer_speed_str); - - g_free(basename); - g_free(file_size_str); - g_free(transfer_speed_str); - } - } - g_object_unref(task); - - g_object_unref(self); -} - static void file_xfer_data_flushed_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) @@ -1913,93 +1777,6 @@ static void file_xfer_flush_callback(SpiceFileTransferTask *xfer_task, file_xfer_flush_async(main_channel, cancellable, file_xfer_data_flushed_cb, xfer_task); } -/* main context */ -static void file_xfer_read_cb(GObject *source_object, - GAsyncResult *res, - gpointer user_data) -{ - SpiceFileTransferTask *self = user_data; - gssize count; - GError *error = NULL; - - self->priv->pending = FALSE; - count = g_input_stream_read_finish(G_INPUT_STREAM(self->priv->file_stream), - res, &error); - /* Check for pending earlier errors */ - if (self->priv->error) { - spice_file_transfer_task_completed(self, error); - return; - } - - if (count > 0 || self->priv->file_size == 0) { - self->priv->read_bytes += count; - g_object_notify(G_OBJECT(self), "progress"); - - if (self->priv->flush_callback) { - self->priv->pending = TRUE; - self->priv->flush_callback(self, self->priv->buffer, count, self->priv->flush_callback_data); - } - } else if (error) { - spice_channel_wakeup(SPICE_CHANNEL(self->priv->channel), FALSE); - spice_file_transfer_task_completed(self, error); - } - /* else EOF, do nothing (wait for VD_AGENT_FILE_XFER_STATUS from agent) */ -} - -/* coroutine context */ -static void file_xfer_continue_read(SpiceFileTransferTask *self) -{ - g_input_stream_read_async(G_INPUT_STREAM(self->priv->file_stream), - self->priv->buffer, - FILE_XFER_CHUNK_SIZE, - G_PRIORITY_DEFAULT, - self->priv->cancellable, - file_xfer_read_cb, - self); - self->priv->pending = TRUE; -} - -/* coroutine context */ -static void spice_file_transfer_task_handle_status(SpiceFileTransferTask *task, - VDAgentFileXferStatusMessage *msg) -{ - GError *error = NULL; - g_return_if_fail(task != NULL); - - SPICE_DEBUG("task %d received response %d", msg->id, msg->result); - - switch (msg->result) { - case VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: - if (task->priv->pending) { - error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, - "transfer received CAN_SEND_DATA in pending state"); - break; - } - file_xfer_continue_read(task); - return; - case VD_AGENT_FILE_XFER_STATUS_CANCELLED: - error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, - "transfer is cancelled by spice agent"); - break; - case VD_AGENT_FILE_XFER_STATUS_ERROR: - error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, - "some errors occurred in the spice agent"); - break; - case VD_AGENT_FILE_XFER_STATUS_SUCCESS: - if (task->priv->pending) - error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, - "transfer received success in pending state"); - break; - default: - g_warn_if_reached(); - error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, - "unhandled status type: %u", msg->result); - break; - } - - spice_file_transfer_task_completed(task, error); -} - /* any context: the message is not flushed immediately, you can wakeup() the channel coroutine or send_msg_queue() */ static void agent_max_clipboard(SpiceMainChannel *self) @@ -2953,88 +2730,6 @@ void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean spice_main_update_display_enabled(channel, id, enabled, TRUE); } -static void spice_file_transfer_task_completed(SpiceFileTransferTask *self, - GError *error) -{ - /* In case of multiple errors we only report the first error */ - if (self->priv->error) - g_clear_error(&error); - if (error) { - gchar *path = g_file_get_path(self->priv->file); - SPICE_DEBUG("File %s xfer failed: %s", - path, error->message); - g_free(path); - self->priv->error = error; - } - - if (self->priv->pending) - return; - - if (!self->priv->file_stream) { - file_xfer_close_cb(NULL, NULL, self); - goto signal; - } - - g_input_stream_close_async(G_INPUT_STREAM(self->priv->file_stream), - G_PRIORITY_DEFAULT, - self->priv->cancellable, - file_xfer_close_cb, - self); - self->priv->pending = TRUE; -signal: - g_signal_emit(self, task_signals[SIGNAL_FINISHED], 0, self->priv->error); -} - - -static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data) -{ - GFileInfo *info; - GFile *file = G_FILE(obj); - GError *error = NULL; - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); - - self->priv->pending = FALSE; - info = g_file_query_info_finish(file, res, &error); - if (error || self->priv->error) - goto failed; - - self->priv->file_size = - g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); - g_signal_emit(self, task_signals[SIGNAL_FILE_INFO], 0, info); - g_object_notify(G_OBJECT(self), "progress"); - - return; - -failed: - spice_file_transfer_task_completed(self, error); -} - -static void file_xfer_read_async_cb(GObject *obj, GAsyncResult *res, gpointer data) -{ - GFile *file = G_FILE(obj); - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); - GError *error = NULL; - - self->priv->pending = FALSE; - self->priv->file_stream = g_file_read_finish(file, res, &error); - if (error || self->priv->error) { - spice_file_transfer_task_completed(self, error); - return; - } - - g_file_query_info_async(self->priv->file, - "standard::*", - G_FILE_QUERY_INFO_NONE, - G_PRIORITY_DEFAULT, - self->priv->cancellable, - file_xfer_info_async_cb, - self); - self->priv->pending = TRUE; -} - -static SpiceFileTransferTask *spice_file_transfer_task_new(SpiceMainChannel *channel, - GFile *file, - GCancellable *cancellable); static void file_xfer_on_file_info(SpiceFileTransferTask *xfer_task, GFileInfo *info, gpointer data) @@ -3299,378 +2994,3 @@ gboolean spice_main_file_copy_finish(SpiceMainChannel *channel, return g_task_propagate_boolean(task, error); } - -static SpiceMainChannel *spice_file_transfer_task_get_channel(SpiceFileTransferTask *self) -{ - g_assert_nonnull(self); - return self->priv->channel; -} - -static GCancellable *spice_file_transfer_task_get_cancellable(SpiceFileTransferTask *self) -{ - g_assert_nonnull(self); - return self->priv->cancellable; -} - -static void spice_file_transfer_task_flush_done(SpiceFileTransferTask *self, GError *error) -{ - g_assert_nonnull(self); - g_return_if_fail(self->priv->pending); - - self->priv->pending = FALSE; - - if (error || self->priv->error) { - spice_file_transfer_task_completed(self, error); - return; - } - - if (spice_util_get_debug()) { - const GTimeSpan interval = 20 * G_TIME_SPAN_SECOND; - gint64 now = g_get_monotonic_time(); - - if (interval < now - self->priv->last_update) { - gchar *basename = g_file_get_basename(self->priv->file); - self->priv->last_update = now; - SPICE_DEBUG("transferred %.2f%% of the file %s", - 100.0 * self->priv->read_bytes / self->priv->file_size, basename); - g_free(basename); - } - } - - /* Read more data */ - file_xfer_continue_read(self); -} - -static GList *spice_file_transfer_task_create_tasks(SpiceMainChannel *channel, - GFile **files, - GFileCopyFlags flags, - GCancellable *cancellable, - SpiceFileTransferTaskFlushCb flush_callback, - gpointer flush_callback_data, - GAsyncReadyCallback callback, - gpointer user_data) -{ - SpiceFileTransferTask *task; - gint i; - GList *tasks = NULL; - - for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) { - GCancellable *task_cancellable = cancellable; - /* if a cancellable object was not provided for the overall operation, - * create a separate object for each file so that they can be cancelled - * separately */ - if (!task_cancellable) - task_cancellable = g_cancellable_new(); - - task = spice_file_transfer_task_new(channel, files[i], task_cancellable); - task->priv->flags = flags; - task->priv->flush_callback = flush_callback; - task->priv->flush_callback_data = flush_callback_data; - task->priv->callback = callback; - task->priv->user_data = user_data; - - CHANNEL_DEBUG(channel, "Insert a xfer task:%d to task list", - task->priv->id); - tasks = g_list_prepend(tasks, task); - - /* if we created a per-task cancellable above, free it */ - if (!cancellable) - g_object_unref(task_cancellable); - } - - return g_list_reverse(tasks); -} - -static void spice_file_transfer_task_start_task(SpiceFileTransferTask *self) -{ - g_assert_nonnull(self); - self->priv->pending = TRUE; - g_file_read_async(self->priv->file, - G_PRIORITY_DEFAULT, - self->priv->cancellable, - file_xfer_read_async_cb, - g_object_ref(self)); -} - -static void -spice_file_transfer_task_get_property(GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); - - switch (property_id) - { - case PROP_TASK_ID: - g_value_set_uint(value, self->priv->id); - break; - case PROP_TASK_FILE: - g_value_set_object(value, self->priv->file); - break; - case PROP_TASK_PROGRESS: - g_value_set_double(value, spice_file_transfer_task_get_progress(self)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); - } -} - -static void -spice_file_transfer_task_set_property(GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec) -{ - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); - - switch (property_id) - { - case PROP_TASK_ID: - self->priv->id = g_value_get_uint(value); - break; - case PROP_TASK_FILE: - self->priv->file = g_value_dup_object(value); - break; - case PROP_TASK_CHANNEL: - self->priv->channel = g_value_dup_object(value); - break; - case PROP_TASK_CANCELLABLE: - self->priv->cancellable = g_value_dup_object(value); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); - } -} - -static void -spice_file_transfer_task_dispose(GObject *object) -{ - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); - - g_clear_object(&self->priv->file); - - G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->dispose(object); -} - -static void -spice_file_transfer_task_finalize(GObject *object) -{ - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); - - g_free(self->priv->buffer); - - G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->finalize(object); -} - -static void -spice_file_transfer_task_constructed(GObject *object) -{ - SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); - - if (spice_util_get_debug()) { - gchar *basename = g_file_get_basename(self->priv->file); - self->priv->start_time = g_get_monotonic_time(); - self->priv->last_update = self->priv->start_time; - - SPICE_DEBUG("transfer of file %s has started", basename); - g_free(basename); - } -} - -static void -spice_file_transfer_task_class_init(SpiceFileTransferTaskClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS(klass); - - g_type_class_add_private(klass, sizeof(SpiceFileTransferTaskPrivate)); - - object_class->get_property = spice_file_transfer_task_get_property; - object_class->set_property = spice_file_transfer_task_set_property; - object_class->finalize = spice_file_transfer_task_finalize; - object_class->dispose = spice_file_transfer_task_dispose; - object_class->constructed = spice_file_transfer_task_constructed; - - /** - * SpiceFileTransferTask:id: - * - * The ID of the file transfer task - * - * Since: 0.31 - **/ - g_object_class_install_property(object_class, PROP_TASK_ID, - g_param_spec_uint("id", - "id", - "The id of the task", - 0, G_MAXUINT, 0, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS)); - - /** - * SpiceFileTransferTask:channel: - * - * The main channel that owns the file transfer task - * - * Since: 0.31 - **/ - g_object_class_install_property(object_class, PROP_TASK_CHANNEL, - g_param_spec_object("channel", - "channel", - "The channel transferring the file", - SPICE_TYPE_MAIN_CHANNEL, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS)); - - /** - * SpiceFileTransferTask:cancellable: - * - * A cancellable object used to cancel the file transfer - * - * Since: 0.31 - **/ - g_object_class_install_property(object_class, PROP_TASK_CANCELLABLE, - g_param_spec_object("cancellable", - "cancellable", - "The object used to cancel the task", - G_TYPE_CANCELLABLE, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS)); - - /** - * SpiceFileTransferTask:file: - * - * The file that is being transferred in this file transfer task - * - * Since: 0.31 - **/ - g_object_class_install_property(object_class, PROP_TASK_FILE, - g_param_spec_object("file", - "File", - "The file being transferred", - G_TYPE_FILE, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS)); - - /** - * SpiceFileTransferTask:progress: - * - * The current state of the file transfer. This value indicates a - * percentage, and ranges from 0 to 100. Listen for change notifications on - * this property to be updated whenever the file transfer progress changes. - * - * Since: 0.31 - **/ - g_object_class_install_property(object_class, PROP_TASK_PROGRESS, - g_param_spec_double("progress", - "Progress", - "The percentage of the file transferred", - 0.0, 100.0, 0.0, - G_PARAM_READABLE | - G_PARAM_STATIC_STRINGS)); - - /** - * SpiceFileTransferTask::file-info - * @task: the file transfer task that emitted the signal - * @file_info: (transfer none): A GFileInfo object to retrieve file - * information. Only keys from standard namespace are supported. - * - * The #SpiceFileTransferTask::file-info signal is emitted just before the file - * transfer effectively starts. - * - * Since: 0.32 - **/ - task_signals[SIGNAL_FILE_INFO] = g_signal_new("file-info", SPICE_TYPE_FILE_TRANSFER_TASK, - G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, - g_cclosure_marshal_VOID__BOXED, - G_TYPE_NONE, 1, - G_TYPE_FILE_INFO); - - /** - * SpiceFileTransferTask::finished: - * @task: the file transfer task that emitted the signal - * @error: (transfer none): the error state of the transfer. Will be %NULL - * if the file transfer was successful. - * - * The #SpiceFileTransferTask::finished signal is emitted when the file - * transfer has completed transferring to the guest. - * - * Since: 0.31 - **/ - task_signals[SIGNAL_FINISHED] = g_signal_new("finished", SPICE_TYPE_FILE_TRANSFER_TASK, - G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, - g_cclosure_marshal_VOID__BOXED, - G_TYPE_NONE, 1, - G_TYPE_ERROR); -} - -static void -spice_file_transfer_task_init(SpiceFileTransferTask *self) -{ - self->priv = FILE_TRANSFER_TASK_PRIVATE(self); - self->priv->buffer = g_malloc0(FILE_XFER_CHUNK_SIZE); -} - -static SpiceFileTransferTask * -spice_file_transfer_task_new(SpiceMainChannel *channel, GFile *file, GCancellable *cancellable) -{ - static uint32_t xfer_id = 0; /* Used to identify task id */ - - return g_object_new(SPICE_TYPE_FILE_TRANSFER_TASK, - "id", xfer_id++, - "file", file, - "channel", channel, - "cancellable", cancellable, - NULL); -} - -/** - * spice_file_transfer_task_get_progress: - * @self: a file transfer task - * - * Convenience function for retrieving the current progress of this file - * transfer task. - * - * Returns: A percentage value between 0 and 100 - * - * Since: 0.31 - **/ -double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self) -{ - if (self->priv->file_size == 0) - return 0.0; - - return (double)self->priv->read_bytes / self->priv->file_size; -} - -/** - * spice_file_transfer_task_cancel: - * @self: a file transfer task - * - * Cancels the file transfer task. Note that depending on how the file transfer - * was initiated, multiple file transfer tasks may share a single - * #SpiceFileTransferTask::cancellable object, so canceling one task may result - * in the cancellation of other tasks. - * - * Since: 0.31 - **/ -void spice_file_transfer_task_cancel(SpiceFileTransferTask *self) -{ - g_cancellable_cancel(self->priv->cancellable); -} - -/** - * spice_file_transfer_task_get_filename: - * @self: a file transfer task - * - * Gets the name of the file being transferred in this task - * - * Returns: (transfer none): The basename of the file - * - * Since: 0.31 - **/ -char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self) -{ - return g_file_get_basename(self->priv->file); -} diff --git a/src/spice-file-transfer-task-priv.h b/src/spice-file-transfer-task-priv.h new file mode 100644 index 0000000..fa28d00 --- /dev/null +++ b/src/spice-file-transfer-task-priv.h @@ -0,0 +1,56 @@ +/* + Copyright (C) 2016 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __SPICE_FILE_TRANSFER_TASK_PRIV_H__ +#define __SPICE_FILE_TRANSFER_TASK_PRIV_H__ + +#include "config.h" + +#include <spice/vd_agent.h> + +#include "spice-client.h" +#include "channel-main.h" +#include "spice-file-transfer-task.h" +#include "spice-channel-priv.h" + +G_BEGIN_DECLS + +typedef void (*SpiceFileTransferTaskFlushCb)(SpiceFileTransferTask *xfer_task, + void *buffer, + gssize count, + gpointer user_data); + +SpiceMainChannel *spice_file_transfer_task_get_channel(SpiceFileTransferTask *self); +GCancellable *spice_file_transfer_task_get_cancellable(SpiceFileTransferTask *self); +void spice_file_transfer_task_flush_done(SpiceFileTransferTask *self, GError *error); +GList *spice_file_transfer_task_create_tasks(SpiceMainChannel *channel, + GFile **files, + GFileCopyFlags flags, + GCancellable *cancellable, + SpiceFileTransferTaskFlushCb flush_callback, + gpointer flush_callback_data, + GAsyncReadyCallback callback, + gpointer user_data); +void spice_file_transfer_task_start_task(SpiceFileTransferTask *self); +void spice_file_transfer_task_completed(SpiceFileTransferTask *self, GError *error); +void spice_file_transfer_task_handle_status(SpiceFileTransferTask *task, + VDAgentFileXferStatusMessage *msg); + + +G_END_DECLS + +#endif /* __SPICE_FILE_TRANSFER_TASK_PRIV_H__ */ diff --git a/src/spice-file-transfer-task.c b/src/spice-file-transfer-task.c new file mode 100644 index 0000000..8d006b5 --- /dev/null +++ b/src/spice-file-transfer-task.c @@ -0,0 +1,697 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010-2016 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include "config.h" + +#include "spice-file-transfer-task-priv.h" + +/** + * SECTION:file-transfer-task + * @short_description: Monitoring file transfers + * @title: File Transfer Task + * @section_id: + * @see_also: #SpiceMainChannel + * @stability: Stable + * @include: spice-client.h + * + * SpiceFileTransferTask is an object that represents a particular file + * transfer between the client and the guest. The properties and signals of the + * object can be used to monitor the status and result of the transfer. The + * Main Channel's #SpiceMainChannel::new-file-transfer signal will be emitted + * whenever a new file transfer task is initiated. + * + * Since: 0.31 + */ +G_DEFINE_TYPE(SpiceFileTransferTask, spice_file_transfer_task, G_TYPE_OBJECT) + +#define FILE_TRANSFER_TASK_PRIVATE(o) \ + (G_TYPE_INSTANCE_GET_PRIVATE((o), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskPrivate)) + +#define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32) + +struct _SpiceFileTransferTaskPrivate +{ + uint32_t id; + gboolean pending; + GFile *file; + SpiceMainChannel *channel; + GFileInputStream *file_stream; + GFileCopyFlags flags; + GCancellable *cancellable; + SpiceFileTransferTaskFlushCb flush_callback; + gpointer flush_callback_data; + GAsyncReadyCallback callback; + gpointer user_data; + char *buffer; + uint64_t read_bytes; + uint64_t file_size; + gint64 start_time; + gint64 last_update; + GError *error; +}; + +enum { + PROP_TASK_ID = 1, + PROP_TASK_CHANNEL, + PROP_TASK_CANCELLABLE, + PROP_TASK_FILE, + PROP_TASK_PROGRESS, +}; + +enum { + SIGNAL_FINISHED, + SIGNAL_FILE_INFO, + LAST_TASK_SIGNAL +}; + +static guint task_signals[LAST_TASK_SIGNAL]; + +/******************************************************************************* + * Helpers + ******************************************************************************/ +static SpiceFileTransferTask * +spice_file_transfer_task_new(SpiceMainChannel *channel, GFile *file, GCancellable *cancellable) +{ + static uint32_t xfer_id = 0; /* Used to identify task id */ + + return g_object_new(SPICE_TYPE_FILE_TRANSFER_TASK, + "id", xfer_id++, + "file", file, + "channel", channel, + "cancellable", cancellable, + NULL); +} + +/* main context */ +static void file_xfer_close_cb(GObject *object, + GAsyncResult *close_res, + gpointer user_data) +{ + GTask *task; + SpiceFileTransferTask *self; + GError *error = NULL; + + self = user_data; + + if (object) { + GInputStream *stream = G_INPUT_STREAM(object); + g_input_stream_close_finish(stream, close_res, &error); + if (error) { + /* This error dont need to report to user, just print a log */ + SPICE_DEBUG("close file error: %s", error->message); + g_clear_error(&error); + } + } + + /* Notify to user that files have been transferred or something error + happened. */ + task = g_task_new(self->priv->channel, + self->priv->cancellable, + self->priv->callback, + self->priv->user_data); + + if (self->priv->error) { + g_task_return_error(task, self->priv->error); + } else { + g_task_return_boolean(task, TRUE); + if (spice_util_get_debug()) { + gint64 now = g_get_monotonic_time(); + gchar *basename = g_file_get_basename(self->priv->file); + double seconds = (double) (now - self->priv->start_time) / G_TIME_SPAN_SECOND; + gchar *file_size_str = g_format_size(self->priv->file_size); + gchar *transfer_speed_str = g_format_size(self->priv->file_size / seconds); + + g_warn_if_fail(self->priv->read_bytes == self->priv->file_size); + SPICE_DEBUG("transferred file %s of %s size in %.1f seconds (%s/s)", + basename, file_size_str, seconds, transfer_speed_str); + + g_free(basename); + g_free(file_size_str); + g_free(transfer_speed_str); + } + } + g_object_unref(task); + + g_object_unref(self); +} + +/* main context */ +static void file_xfer_read_cb(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SpiceFileTransferTask *self = user_data; + gssize count; + GError *error = NULL; + + self->priv->pending = FALSE; + count = g_input_stream_read_finish(G_INPUT_STREAM(self->priv->file_stream), + res, &error); + /* Check for pending earlier errors */ + if (self->priv->error) { + spice_file_transfer_task_completed(self, error); + return; + } + + if (count > 0 || self->priv->file_size == 0) { + self->priv->read_bytes += count; + g_object_notify(G_OBJECT(self), "progress"); + + if (self->priv->flush_callback) { + self->priv->pending = TRUE; + self->priv->flush_callback(self, self->priv->buffer, count, self->priv->flush_callback_data); + } + } else if (error) { + spice_channel_wakeup(SPICE_CHANNEL(self->priv->channel), FALSE); + spice_file_transfer_task_completed(self, error); + } + /* else EOF, do nothing (wait for VD_AGENT_FILE_XFER_STATUS from agent) */ +} + +/* coroutine context */ +static void file_xfer_continue_read(SpiceFileTransferTask *self) +{ + g_input_stream_read_async(G_INPUT_STREAM(self->priv->file_stream), + self->priv->buffer, + FILE_XFER_CHUNK_SIZE, + G_PRIORITY_DEFAULT, + self->priv->cancellable, + file_xfer_read_cb, + self); + self->priv->pending = TRUE; +} + +static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data) +{ + GFileInfo *info; + GFile *file = G_FILE(obj); + GError *error = NULL; + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); + + self->priv->pending = FALSE; + info = g_file_query_info_finish(file, res, &error); + if (error || self->priv->error) + goto failed; + + self->priv->file_size = + g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); + g_signal_emit(self, task_signals[SIGNAL_FILE_INFO], 0, info); + g_object_notify(G_OBJECT(self), "progress"); + + return; + +failed: + spice_file_transfer_task_completed(self, error); +} + +static void file_xfer_read_async_cb(GObject *obj, GAsyncResult *res, gpointer data) +{ + GFile *file = G_FILE(obj); + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); + GError *error = NULL; + + self->priv->pending = FALSE; + self->priv->file_stream = g_file_read_finish(file, res, &error); + if (error || self->priv->error) { + spice_file_transfer_task_completed(self, error); + return; + } + + g_file_query_info_async(self->priv->file, + "standard::*", + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + self->priv->cancellable, + file_xfer_info_async_cb, + self); + self->priv->pending = TRUE; +} + +/******************************************************************************* + * Internal API + ******************************************************************************/ + +/* coroutine context */ +G_GNUC_INTERNAL +void spice_file_transfer_task_handle_status(SpiceFileTransferTask *task, + VDAgentFileXferStatusMessage *msg) +{ + GError *error = NULL; + g_return_if_fail(task != NULL); + + SPICE_DEBUG("task %d received response %d", msg->id, msg->result); + + switch (msg->result) { + case VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: + if (task->priv->pending) { + error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, + "transfer received CAN_SEND_DATA in pending state"); + break; + } + file_xfer_continue_read(task); + return; + case VD_AGENT_FILE_XFER_STATUS_CANCELLED: + error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, + "transfer is cancelled by spice agent"); + break; + case VD_AGENT_FILE_XFER_STATUS_ERROR: + error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, + "some errors occurred in the spice agent"); + break; + case VD_AGENT_FILE_XFER_STATUS_SUCCESS: + if (task->priv->pending) + error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, + "transfer received success in pending state"); + break; + default: + g_warn_if_reached(); + error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, + "unhandled status type: %u", msg->result); + break; + } + + spice_file_transfer_task_completed(task, error); +} + +G_GNUC_INTERNAL +void spice_file_transfer_task_completed(SpiceFileTransferTask *self, + GError *error) +{ + /* In case of multiple errors we only report the first error */ + if (self->priv->error) + g_clear_error(&error); + if (error) { + gchar *path = g_file_get_path(self->priv->file); + SPICE_DEBUG("File %s xfer failed: %s", + path, error->message); + g_free(path); + self->priv->error = error; + } + + if (self->priv->pending) + return; + + if (!self->priv->file_stream) { + file_xfer_close_cb(NULL, NULL, self); + goto signal; + } + + g_input_stream_close_async(G_INPUT_STREAM(self->priv->file_stream), + G_PRIORITY_DEFAULT, + self->priv->cancellable, + file_xfer_close_cb, + self); + self->priv->pending = TRUE; +signal: + g_signal_emit(self, task_signals[SIGNAL_FINISHED], 0, self->priv->error); +} + +G_GNUC_INTERNAL +SpiceMainChannel *spice_file_transfer_task_get_channel(SpiceFileTransferTask *self) +{ + g_assert_nonnull(self); + return self->priv->channel; +} + +G_GNUC_INTERNAL +GCancellable *spice_file_transfer_task_get_cancellable(SpiceFileTransferTask *self) +{ + g_assert_nonnull(self); + return self->priv->cancellable; +} + +G_GNUC_INTERNAL +void spice_file_transfer_task_flush_done(SpiceFileTransferTask *self, GError *error) +{ + g_assert_nonnull(self); + g_return_if_fail(self->priv->pending); + + self->priv->pending = FALSE; + + if (error || self->priv->error) { + spice_file_transfer_task_completed(self, error); + return; + } + + if (spice_util_get_debug()) { + const GTimeSpan interval = 20 * G_TIME_SPAN_SECOND; + gint64 now = g_get_monotonic_time(); + + if (interval < now - self->priv->last_update) { + gchar *basename = g_file_get_basename(self->priv->file); + self->priv->last_update = now; + SPICE_DEBUG("transferred %.2f%% of the file %s", + 100.0 * self->priv->read_bytes / self->priv->file_size, basename); + g_free(basename); + } + } + + /* Read more data */ + file_xfer_continue_read(self); +} + +G_GNUC_INTERNAL +GList *spice_file_transfer_task_create_tasks(SpiceMainChannel *channel, + GFile **files, + GFileCopyFlags flags, + GCancellable *cancellable, + SpiceFileTransferTaskFlushCb flush_callback, + gpointer flush_callback_data, + GAsyncReadyCallback callback, + gpointer user_data) +{ + SpiceFileTransferTask *task; + gint i; + GList *tasks = NULL; + + for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) { + GCancellable *task_cancellable = cancellable; + /* if a cancellable object was not provided for the overall operation, + * create a separate object for each file so that they can be cancelled + * separately */ + if (!task_cancellable) + task_cancellable = g_cancellable_new(); + + task = spice_file_transfer_task_new(channel, files[i], task_cancellable); + task->priv->flags = flags; + task->priv->flush_callback = flush_callback; + task->priv->flush_callback_data = flush_callback_data; + task->priv->callback = callback; + task->priv->user_data = user_data; + + CHANNEL_DEBUG(channel, "Insert a xfer task:%d to task list", + task->priv->id); + tasks = g_list_prepend(tasks, task); + + /* if we created a per-task cancellable above, free it */ + if (!cancellable) + g_object_unref(task_cancellable); + } + + return g_list_reverse(tasks); +} + +G_GNUC_INTERNAL +void spice_file_transfer_task_start_task(SpiceFileTransferTask *self) +{ + g_assert_nonnull(self); + self->priv->pending = TRUE; + g_file_read_async(self->priv->file, + G_PRIORITY_DEFAULT, + self->priv->cancellable, + file_xfer_read_async_cb, + g_object_ref(self)); +} + +/******************************************************************************* + * External API + ******************************************************************************/ + +/** + * spice_file_transfer_task_get_progress: + * @self: a file transfer task + * + * Convenience function for retrieving the current progress of this file + * transfer task. + * + * Returns: A percentage value between 0 and 100 + * + * Since: 0.31 + **/ +double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self) +{ + if (self->priv->file_size == 0) + return 0.0; + + return (double)self->priv->read_bytes / self->priv->file_size; +} + +/** + * spice_file_transfer_task_cancel: + * @self: a file transfer task + * + * Cancels the file transfer task. Note that depending on how the file transfer + * was initiated, multiple file transfer tasks may share a single + * #SpiceFileTransferTask::cancellable object, so canceling one task may result + * in the cancellation of other tasks. + * + * Since: 0.31 + **/ +void spice_file_transfer_task_cancel(SpiceFileTransferTask *self) +{ + g_cancellable_cancel(self->priv->cancellable); +} + +/** + * spice_file_transfer_task_get_filename: + * @self: a file transfer task + * + * Gets the name of the file being transferred in this task + * + * Returns: (transfer none): The basename of the file + * + * Since: 0.31 + **/ +char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self) +{ + return g_file_get_basename(self->priv->file); +} + +/******************************************************************************* + * GObject + ******************************************************************************/ + +static void +spice_file_transfer_task_get_property(GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); + + switch (property_id) + { + case PROP_TASK_ID: + g_value_set_uint(value, self->priv->id); + break; + case PROP_TASK_FILE: + g_value_set_object(value, self->priv->file); + break; + case PROP_TASK_PROGRESS: + g_value_set_double(value, spice_file_transfer_task_get_progress(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +spice_file_transfer_task_set_property(GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); + + switch (property_id) + { + case PROP_TASK_ID: + self->priv->id = g_value_get_uint(value); + break; + case PROP_TASK_FILE: + self->priv->file = g_value_dup_object(value); + break; + case PROP_TASK_CHANNEL: + self->priv->channel = g_value_dup_object(value); + break; + case PROP_TASK_CANCELLABLE: + self->priv->cancellable = g_value_dup_object(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +spice_file_transfer_task_dispose(GObject *object) +{ + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); + + g_clear_object(&self->priv->file); + + G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->dispose(object); +} + +static void +spice_file_transfer_task_finalize(GObject *object) +{ + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); + + g_free(self->priv->buffer); + + G_OBJECT_CLASS(spice_file_transfer_task_parent_class)->finalize(object); +} + +static void +spice_file_transfer_task_constructed(GObject *object) +{ + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(object); + + if (spice_util_get_debug()) { + gchar *basename = g_file_get_basename(self->priv->file); + self->priv->start_time = g_get_monotonic_time(); + self->priv->last_update = self->priv->start_time; + + SPICE_DEBUG("transfer of file %s has started", basename); + g_free(basename); + } +} + +static void +spice_file_transfer_task_class_init(SpiceFileTransferTaskClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + g_type_class_add_private(klass, sizeof(SpiceFileTransferTaskPrivate)); + + object_class->get_property = spice_file_transfer_task_get_property; + object_class->set_property = spice_file_transfer_task_set_property; + object_class->finalize = spice_file_transfer_task_finalize; + object_class->dispose = spice_file_transfer_task_dispose; + object_class->constructed = spice_file_transfer_task_constructed; + + /** + * SpiceFileTransferTask:id: + * + * The ID of the file transfer task + * + * Since: 0.31 + **/ + g_object_class_install_property(object_class, PROP_TASK_ID, + g_param_spec_uint("id", + "id", + "The id of the task", + 0, G_MAXUINT, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * SpiceFileTransferTask:channel: + * + * The main channel that owns the file transfer task + * + * Since: 0.31 + **/ + g_object_class_install_property(object_class, PROP_TASK_CHANNEL, + g_param_spec_object("channel", + "channel", + "The channel transferring the file", + SPICE_TYPE_MAIN_CHANNEL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * SpiceFileTransferTask:cancellable: + * + * A cancellable object used to cancel the file transfer + * + * Since: 0.31 + **/ + g_object_class_install_property(object_class, PROP_TASK_CANCELLABLE, + g_param_spec_object("cancellable", + "cancellable", + "The object used to cancel the task", + G_TYPE_CANCELLABLE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * SpiceFileTransferTask:file: + * + * The file that is being transferred in this file transfer task + * + * Since: 0.31 + **/ + g_object_class_install_property(object_class, PROP_TASK_FILE, + g_param_spec_object("file", + "File", + "The file being transferred", + G_TYPE_FILE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * SpiceFileTransferTask:progress: + * + * The current state of the file transfer. This value indicates a + * percentage, and ranges from 0 to 100. Listen for change notifications on + * this property to be updated whenever the file transfer progress changes. + * + * Since: 0.31 + **/ + g_object_class_install_property(object_class, PROP_TASK_PROGRESS, + g_param_spec_double("progress", + "Progress", + "The percentage of the file transferred", + 0.0, 100.0, 0.0, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * SpiceFileTransferTask::file-info + * @task: the file transfer task that emitted the signal + * @file_info: (transfer none): A GFileInfo object to retrieve file + * information. Only keys from standard namespace are supported. + * + * The #SpiceFileTransferTask::file-info signal is emitted just before the file + * transfer effectively starts. + * + * Since: 0.32 + **/ + task_signals[SIGNAL_FILE_INFO] = g_signal_new("file-info", SPICE_TYPE_FILE_TRANSFER_TASK, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__BOXED, + G_TYPE_NONE, 1, + G_TYPE_FILE_INFO); + + /** + * SpiceFileTransferTask::finished: + * @task: the file transfer task that emitted the signal + * @error: (transfer none): the error state of the transfer. Will be %NULL + * if the file transfer was successful. + * + * The #SpiceFileTransferTask::finished signal is emitted when the file + * transfer has completed transferring to the guest. + * + * Since: 0.31 + **/ + task_signals[SIGNAL_FINISHED] = g_signal_new("finished", SPICE_TYPE_FILE_TRANSFER_TASK, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__BOXED, + G_TYPE_NONE, 1, + G_TYPE_ERROR); +} + +static void +spice_file_transfer_task_init(SpiceFileTransferTask *self) +{ + self->priv = FILE_TRANSFER_TASK_PRIVATE(self); + self->priv->buffer = g_malloc0(FILE_XFER_CHUNK_SIZE); +} -- 2.5.5 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel