/* 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 . */ #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 */ struct _SpiceFileTransferTask { GObject parent; uint32_t id; gboolean completed; gboolean pending; GFile *file; SpiceMainChannel *channel; GFileInputStream *file_stream; GFileCopyFlags flags; GCancellable *cancellable; GAsyncReadyCallback callback; gpointer user_data; char *buffer; uint64_t read_bytes; uint64_t file_size; gint64 start_time; gint64 last_update; GError *error; }; struct _SpiceFileTransferTaskClass { GObjectClass parent_class; }; G_DEFINE_TYPE(SpiceFileTransferTask, spice_file_transfer_task, G_TYPE_OBJECT) #define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32) enum { PROP_TASK_ID = 1, PROP_TASK_CHANNEL, PROP_TASK_CANCELLABLE, PROP_TASK_FILE, PROP_TASK_TOTAL_BYTES, PROP_TASK_TRANSFERRED_BYTES, PROP_TASK_PROGRESS, }; enum { SIGNAL_FINISHED, LAST_TASK_SIGNAL }; static guint task_signals[LAST_TASK_SIGNAL]; /******************************************************************************* * Helpers ******************************************************************************/ static SpiceFileTransferTask * spice_file_transfer_task_new(SpiceMainChannel *channel, GFile *file, GFileCopyFlags flags, GCancellable *cancellable) { static uint32_t xfer_id = 1; /* Used to identify task id */ GCancellable *task_cancellable = cancellable; SpiceFileTransferTask *self; /* 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(); self = g_object_new(SPICE_TYPE_FILE_TRANSFER_TASK, "id", xfer_id++, "file", file, "channel", channel, "cancellable", task_cancellable, NULL); self->flags = flags; /* if we created a GCancellable above, unref it */ if (!cancellable) g_object_unref(task_cancellable); return self; } static void spice_file_transfer_task_query_info_cb(GObject *obj, GAsyncResult *res, gpointer user_data) { SpiceFileTransferTask *self; GFileInfo *info; GTask *task; GError *error = NULL; task = G_TASK(user_data); self = g_task_get_source_object(task); g_return_if_fail(self->pending == TRUE); self->pending = FALSE; info = g_file_query_info_finish(G_FILE(obj), res, &error); if (self->error) { g_clear_object(&info); g_clear_error(&error); /* Return error previously reported */ g_task_return_error(task, g_error_copy(self->error)); g_object_unref(task); return; } else if (error) { g_task_return_error(task, error); g_object_unref(task); return; } self->file_size = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); /* SpiceFileTransferTask's init is done, handshake for file-transfer will * start soon. First "progress" can be emitted ~ 0% */ g_object_notify(G_OBJECT(self), "total-bytes"); g_object_notify(G_OBJECT(self), "progress"); g_task_return_pointer(task, info, g_object_unref); g_object_unref(task); } static void spice_file_transfer_task_read_file_cb(GObject *obj, GAsyncResult *res, gpointer user_data) { SpiceFileTransferTask *self; GTask *task; GError *error = NULL; task = G_TASK(user_data); self = g_task_get_source_object(task); g_return_if_fail(self->pending == TRUE); self->file_stream = g_file_read_finish(G_FILE(obj), res, &error); if (self->error) { g_clear_error(&error); /* Return error previously reported */ self->pending = FALSE; g_task_return_error(task, g_error_copy(self->error)); g_object_unref(task); return; } else if (error) { self->pending = FALSE; g_task_return_error(task, error); g_object_unref(task); return; } g_file_query_info_async(self->file, "standard::*", G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, self->cancellable, spice_file_transfer_task_query_info_cb, task); } static void spice_file_transfer_task_read_stream_cb(GObject *source_object, GAsyncResult *res, gpointer userdata) { SpiceFileTransferTask *self; GTask *task; gssize nbytes; GError *error = NULL; task = G_TASK(userdata); self = g_task_get_source_object(task); g_return_if_fail(self->pending == TRUE); self->pending = FALSE; nbytes = g_input_stream_read_finish(G_INPUT_STREAM(self->file_stream), res, &error); if (self->error) { g_clear_error(&error); /* On any pending error on SpiceFileTransferTask */ g_task_return_error(task, g_error_copy(self->error)); g_object_unref(task); return; } else if (error) { g_task_return_error(task, error); g_object_unref(task); return; } self->read_bytes += nbytes; if (spice_util_get_debug()) { const GTimeSpan interval = 20 * G_TIME_SPAN_SECOND; gint64 now = g_get_monotonic_time(); if (interval < now - self->last_update) { gchar *basename = g_file_get_basename(self->file); self->last_update = now; SPICE_DEBUG("read %.2f%% of the file %s", 100.0 * self->read_bytes / self->file_size, basename); g_free(basename); } } g_task_return_int(task, nbytes); g_object_unref(task); } /* main context */ static void spice_file_transfer_task_close_stream_cb(GObject *object, GAsyncResult *close_res, gpointer user_data) { 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); } } if (self->error == NULL && spice_util_get_debug()) { gint64 now = g_get_monotonic_time(); gchar *basename = g_file_get_basename(self->file); double seconds = (double) (now - self->start_time) / G_TIME_SPAN_SECOND; gchar *file_size_str = g_format_size(self->file_size); gchar *transfer_speed_str = g_format_size(self->file_size / seconds); g_warn_if_fail(self->read_bytes == self->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(self); } /******************************************************************************* * Internal API ******************************************************************************/ G_GNUC_INTERNAL void spice_file_transfer_task_completed(SpiceFileTransferTask *self, GError *error) { self->completed = TRUE; /* In case of multiple errors we only report the first error */ if (self->error) g_clear_error(&error); if (error) { gchar *path = g_file_get_path(self->file); SPICE_DEBUG("File %s xfer failed: %s", path, error->message); g_free(path); self->error = error; } if (self->pending) { /* Complete but pending is okay only if error is set */ if (self->error == NULL) { self->error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, "Cannot complete task in pending state"); } return; } if (!self->file_stream) { spice_file_transfer_task_close_stream_cb(NULL, NULL, self); goto signal; } g_input_stream_close_async(G_INPUT_STREAM(self->file_stream), G_PRIORITY_DEFAULT, self->cancellable, spice_file_transfer_task_close_stream_cb, self); self->pending = TRUE; signal: g_signal_emit(self, task_signals[SIGNAL_FINISHED], 0, self->error); /* SpiceFileTransferTask unref is done after input stream is closed */ } G_GNUC_INTERNAL guint32 spice_file_transfer_task_get_id(SpiceFileTransferTask *self) { g_return_val_if_fail(self != NULL, 0); return self->id; } G_GNUC_INTERNAL SpiceMainChannel *spice_file_transfer_task_get_channel(SpiceFileTransferTask *self) { g_return_val_if_fail(self != NULL, NULL); return self->channel; } G_GNUC_INTERNAL GCancellable *spice_file_transfer_task_get_cancellable(SpiceFileTransferTask *self) { g_return_val_if_fail(self != NULL, NULL); return self->cancellable; } /* Helper function which only creates a SpiceFileTransferTask per GFile * in @files and returns a HashTable mapping task-id to the task itself * The SpiceFileTransferTask created here has two references, one should be * freed by spice_file_transfer_task_close_stream_cb() after * spice_file_transfer_task_completed() is called and the other reference * belongs to the caller and should be freed upon GHashTable destruction */ G_GNUC_INTERNAL GHashTable *spice_file_transfer_task_create_tasks(GFile **files, SpiceMainChannel *channel, GFileCopyFlags flags, GCancellable *cancellable) { GHashTable *xfer_ht; gint i; g_return_val_if_fail(files != NULL && files[0] != NULL, NULL); xfer_ht = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) { SpiceFileTransferTask *xfer_task; guint32 task_id; xfer_task = spice_file_transfer_task_new(channel, files[i], flags, cancellable); task_id = spice_file_transfer_task_get_id(xfer_task); g_hash_table_insert(xfer_ht, GUINT_TO_POINTER(task_id), g_object_ref(xfer_task)); } return xfer_ht; } G_GNUC_INTERNAL void spice_file_transfer_task_init_task_async(SpiceFileTransferTask *self, GAsyncReadyCallback callback, gpointer userdata) { GTask *task; g_return_if_fail(self != NULL); g_return_if_fail(self->pending == FALSE); task = g_task_new(self, self->cancellable, callback, userdata); self->pending = TRUE; g_file_read_async(self->file, G_PRIORITY_DEFAULT, self->cancellable, spice_file_transfer_task_read_file_cb, task); } G_GNUC_INTERNAL GFileInfo *spice_file_transfer_task_init_task_finish(SpiceFileTransferTask *self, GAsyncResult *result, GError **error) { GTask *task = G_TASK(result); g_return_val_if_fail(self != NULL, NULL); return g_task_propagate_pointer(task, error); } /* Any context */ G_GNUC_INTERNAL void spice_file_transfer_task_read_async(SpiceFileTransferTask *self, GAsyncReadyCallback callback, gpointer userdata) { GTask *task; g_return_if_fail(self != NULL); if (self->pending) { g_task_report_new_error(self, callback, userdata, spice_file_transfer_task_read_async, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, "Cannot read data in pending state"); return; } /* Notify the progress prior the read to make the info be related to the * data that was already sent. To notify the 100% (completed), channel-main * should call read-async when it expects EOF. */ g_object_notify(G_OBJECT(self), "progress"); g_object_notify(G_OBJECT(self), "transferred-bytes"); task = g_task_new(self, self->cancellable, callback, userdata); if (self->read_bytes == self->file_size) { /* channel-main might request data after reading the whole file as it * expects EOF. Let's return immediately its request as we don't want to * reach a state where agent says file-transfer SUCCEED but we are in a * PENDING state in SpiceFileTransferTask due reading in idle */ g_task_return_int(task, 0); g_object_unref(task); return; } self->pending = TRUE; g_input_stream_read_async(G_INPUT_STREAM(self->file_stream), self->buffer, FILE_XFER_CHUNK_SIZE, G_PRIORITY_DEFAULT, self->cancellable, spice_file_transfer_task_read_stream_cb, task); } G_GNUC_INTERNAL gssize spice_file_transfer_task_read_finish(SpiceFileTransferTask *self, GAsyncResult *result, char **buffer, GError **error) { gssize nbytes; GTask *task = G_TASK(result); g_return_val_if_fail(self != NULL, -1); nbytes = g_task_propagate_int(task, error); if (nbytes >= 0 && buffer != NULL) *buffer = self->buffer; return nbytes; } G_GNUC_INTERNAL gboolean spice_file_transfer_task_is_completed(SpiceFileTransferTask *self) { g_return_val_if_fail(self != NULL, FALSE); /* File transfer is considered over at the first time it is marked as * complete with spice_file_transfer_task_completed. */ return self->completed; } /******************************************************************************* * 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 fractional value between 0 and 1.0 * * Since: 0.31 **/ double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self) { g_return_val_if_fail(SPICE_IS_FILE_TRANSFER_TASK(self), 0.0); if (self->file_size == 0) return 0.0; return (double)self->read_bytes / self->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_return_if_fail(SPICE_IS_FILE_TRANSFER_TASK(self)); g_cancellable_cancel(self->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 full): The basename of the file * * Since: 0.31 **/ char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self) { g_return_val_if_fail(SPICE_IS_FILE_TRANSFER_TASK(self), NULL); return g_file_get_basename(self->file); } /** * spice_file_transfer_task_get_total_bytes: * @self: a file transfer task * * Gets the total size in bytes of the file transfer. * * Returns: The total size of the file transfer * * Since: 0.33 **/ guint64 spice_file_transfer_task_get_total_bytes(SpiceFileTransferTask *self) { g_return_val_if_fail(SPICE_IS_FILE_TRANSFER_TASK(self), 0); return self->file_size; } /** * spice_file_transfer_task_get_transferred_bytes: * @self: a file transfer task * * Gets the number of bytes that have been transferred so far. * * Returns: The number of transferred bytes * * Since: 0.33 **/ guint64 spice_file_transfer_task_get_transferred_bytes(SpiceFileTransferTask *self) { g_return_val_if_fail(SPICE_IS_FILE_TRANSFER_TASK(self), 0); return self->read_bytes; } /******************************************************************************* * 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->id); break; case PROP_TASK_FILE: g_value_set_object(value, self->file); break; case PROP_TASK_TOTAL_BYTES: g_value_set_uint64(value, spice_file_transfer_task_get_total_bytes(self)); break; case PROP_TASK_TRANSFERRED_BYTES: g_value_set_uint64(value, spice_file_transfer_task_get_transferred_bytes(self)); 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->id = g_value_get_uint(value); break; case PROP_TASK_FILE: self->file = g_value_dup_object(value); break; case PROP_TASK_CHANNEL: self->channel = g_value_dup_object(value); break; case PROP_TASK_CANCELLABLE: self->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->file); g_clear_object(&self->file_stream); g_clear_error(&self->error); 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->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->file); self->start_time = g_get_monotonic_time(); self->last_update = self->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); 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:total-bytes: * * The total size in bytes of this file transfer. * * Since: 0.33 **/ g_object_class_install_property(object_class, PROP_TASK_TOTAL_BYTES, g_param_spec_uint64("total-bytes", "Total bytes", "The size in bytes of the file transferred", 0, G_MAXUINT64, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceFileTransferTask:transferred-bytes: * * The number of bytes that have been transferred so far. * * Since: 0.33 **/ g_object_class_install_property(object_class, PROP_TASK_TRANSFERRED_BYTES, g_param_spec_uint64("transferred-bytes", "Transferred bytes", "The number of bytes transferred", 0, G_MAXUINT64, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceFileTransferTask:progress: * * The current state of the file transfer. This value indicates a * fraction, and ranges from 0 to 1.0. 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, 1.0, 0.0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * 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->buffer = g_malloc0(FILE_XFER_CHUNK_SIZE); }