diff options
-rw-r--r-- | doc/reference/spice-gtk-docs.xml | 1 | ||||
-rw-r--r-- | doc/reference/spice-gtk-sections.txt | 21 | ||||
-rw-r--r-- | doc/reference/spice-gtk.types | 2 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/channel-main.c | 582 | ||||
-rw-r--r-- | src/map-file | 5 | ||||
-rw-r--r-- | src/spice-client.h | 1 | ||||
-rw-r--r-- | src/spice-file-transfer-task.h | 68 | ||||
-rw-r--r-- | src/spice-glib-sym-file | 5 | ||||
-rw-r--r-- | src/spicy.c | 152 |
10 files changed, 703 insertions, 136 deletions
diff --git a/doc/reference/spice-gtk-docs.xml b/doc/reference/spice-gtk-docs.xml index de68004..db5dd3d 100644 --- a/doc/reference/spice-gtk-docs.xml +++ b/doc/reference/spice-gtk-docs.xml @@ -55,6 +55,7 @@ <xi:include href="xml/spice-util.xml"/> <xi:include href="xml/spice-version.xml"/> <xi:include href="xml/spice-uri.xml"/> + <xi:include href="xml/file-transfer-task.xml"/> </chapter> </part> diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt index fe24f9f..d8c4c79 100644 --- a/doc/reference/spice-gtk-sections.txt +++ b/doc/reference/spice-gtk-sections.txt @@ -495,3 +495,24 @@ spice_webdav_channel_get_type <SUBSECTION Private> SpiceWebdavChannelPrivate </SECTION> + +<SECTION> +<FILE>file-transfer-task</FILE> +<TITLE>SpiceFileTransferTask</TITLE> +SpiceFileTransferTask +SpiceFileTransferTaskClass +<SUBSECTION> +spice_file_transfer_task_get_progress +spice_file_transfer_task_get_filename +spice_file_transfer_task_cancel +<SUBSECTION Standard> +SPICE_FILE_TRANSFER_TASK +SPICE_IS_FILE_TRANSFER_TASK +SPICE_TYPE_FILE_TRANSFER_TASK +spice_file_transfer_task_get_type +SPICE_FILE_TRANSFER_TASK_CLASS +SPICE_IS_FILE_TRANSFER_TASK_CLASS +SPICE_FILE_TRANSFER_TASK_GET_CLASS +<SUBSECTION Private> +SpiceFileTransferTaskPrivate +</SECTION> diff --git a/doc/reference/spice-gtk.types b/doc/reference/spice-gtk.types index acd616d..e14ae1b 100644 --- a/doc/reference/spice-gtk.types +++ b/doc/reference/spice-gtk.types @@ -20,6 +20,7 @@ #include "smartcard-manager.h" #include "usb-device-manager.h" #include "usb-device-widget.h" +#include "spice-file-transfer-task.h" spice_audio_get_type spice_channel_event_get_type @@ -45,3 +46,4 @@ spice_usb_device_manager_get_type spice_usb_device_widget_get_type spice_port_channel_get_type spice_webdav_channel_get_type +spice_file_transfer_task_get_type diff --git a/src/Makefile.am b/src/Makefile.am index aa5d2b1..0c40c48 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -135,6 +135,7 @@ SPICE_GTK_SOURCES_COMMON = \ spice-gtk-session-priv.h \ spice-widget.c \ spice-widget-priv.h \ + spice-file-transfer-task.h \ vncdisplaykeymap.c \ vncdisplaykeymap.h \ spice-grabsequence.c \ @@ -317,6 +318,7 @@ libspice_client_glibinclude_HEADERS = \ channel-webdav.h \ usb-device-manager.h \ smartcard-manager.h \ + spice-file-transfer-task.h \ $(NULL) nodist_libspice_client_glibinclude_HEADERS = \ diff --git a/src/channel-main.c b/src/channel-main.c index b16898d..ffeb0ba 100644 --- a/src/channel-main.c +++ b/src/channel-main.c @@ -31,6 +31,7 @@ #include "spice-channel-priv.h" #include "spice-session-priv.h" #include "spice-audio-priv.h" +#include "spice-file-transfer-task.h" /** * SECTION:channel-main @@ -55,8 +56,33 @@ 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 struct SpiceFileXferTask { +struct _SpiceFileTransferTaskPrivate + +/* private */ +{ uint32_t id; gboolean pending; GFile *file; @@ -68,13 +94,28 @@ typedef struct SpiceFileXferTask { gpointer progress_callback_data; GAsyncReadyCallback callback; gpointer user_data; - char buffer[FILE_XFER_CHUNK_SIZE]; + char *buffer; uint64_t read_bytes; uint64_t file_size; gint64 start_time; gint64 last_update; GError *error; -} SpiceFileXferTask; +}; + +enum { + PROP_TASK_ID = 1, + PROP_TASK_CHANNEL, + PROP_TASK_CANCELLABLE, + PROP_TASK_FILE, + PROP_TASK_PROGRESS, +}; + +enum { + SIGNAL_FINISHED, + LAST_TASK_SIGNAL +}; + +static guint task_signals[LAST_TASK_SIGNAL]; typedef enum { DISPLAY_UNDEFINED, @@ -168,6 +209,7 @@ enum { SPICE_MAIN_CLIPBOARD_SELECTION_REQUEST, SPICE_MAIN_CLIPBOARD_SELECTION_RELEASE, SPICE_MIGRATION_STARTED, + SPICE_MAIN_NEW_FILE_TRANSFER, SPICE_MAIN_LAST_SIGNAL, }; @@ -181,8 +223,8 @@ 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(SpiceFileXferTask *task); -static void file_xfer_completed(SpiceFileXferTask *task, GError *error); +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); @@ -245,7 +287,8 @@ static void spice_main_channel_init(SpiceMainChannel *channel) c = channel->priv = SPICE_MAIN_CHANNEL_GET_PRIVATE(channel); c->agent_msg_queue = g_queue_new(); - c->file_xfer_tasks = g_hash_table_new(g_direct_hash, g_direct_equal); + c->file_xfer_tasks = g_hash_table_new_full(g_direct_hash, g_direct_equal, + NULL, g_object_unref); c->cancellable_volume_info = g_cancellable_new(); spice_main_channel_reset_capabilties(SPICE_CHANNEL(channel)); @@ -409,11 +452,11 @@ static void spice_main_channel_reset_agent(SpiceMainChannel *channel) tasks = g_hash_table_get_values(c->file_xfer_tasks); for (l = tasks; l != NULL; l = l->next) { - SpiceFileXferTask *task = (SpiceFileXferTask *)l->data; + SpiceFileTransferTask *task = (SpiceFileTransferTask *)l->data; error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, "Agent connection closed"); - file_xfer_completed(task, error); + spice_file_transfer_task_completed(task, error); } g_list_free(tasks); file_xfer_flushed(channel, FALSE); @@ -828,6 +871,28 @@ static void spice_main_channel_class_init(SpiceMainChannelClass *klass) 1, G_TYPE_OBJECT); + /** + * SpiceMainChannel::new-file-transfer: + * @main: the #SpiceMainChannel that emitted the signal + * @task: a #SpiceFileTransferTask + * + * This signal is emitted when a new file transfer task has been initiated + * on this channel. Client applications may take a reference on the @task + * object and use it to monitor the status of the file transfer task. + * + * Since: 0.31 + **/ + signals[SPICE_MAIN_NEW_FILE_TRANSFER] = + g_signal_new("new-file-transfer", + G_OBJECT_CLASS_TYPE(gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); + g_type_class_add_private(klass, sizeof(SpiceMainChannelPrivate)); channel_set_handlers(SPICE_CHANNEL_CLASS(klass)); } @@ -1691,31 +1756,16 @@ static void main_handle_agent_disconnected(SpiceChannel *channel, SpiceMsgIn *in agent_stopped(SPICE_MAIN_CHANNEL(channel)); } -static void file_xfer_task_free(SpiceFileXferTask *task) -{ - SpiceMainChannelPrivate *c; - - g_return_if_fail(task != NULL); - - c = task->channel->priv; - g_hash_table_remove(c->file_xfer_tasks, GUINT_TO_POINTER(task->id)); - - g_clear_object(&task->channel); - g_clear_object(&task->file); - g_clear_object(&task->file_stream); - g_free(task); -} - /* main context */ static void file_xfer_close_cb(GObject *object, GAsyncResult *close_res, gpointer user_data) { GSimpleAsyncResult *res; - SpiceFileXferTask *task; + SpiceFileTransferTask *self; GError *error = NULL; - task = user_data; + self = user_data; if (object) { GInputStream *stream = G_INPUT_STREAM(object); @@ -1729,23 +1779,23 @@ static void file_xfer_close_cb(GObject *object, /* Notify to user that files have been transferred or something error happened. */ - res = g_simple_async_result_new(G_OBJECT(task->channel), - task->callback, - task->user_data, + res = g_simple_async_result_new(G_OBJECT(self->priv->channel), + self->priv->callback, + self->priv->user_data, spice_main_file_copy_async); - if (task->error) { - g_simple_async_result_take_error(res, task->error); + if (self->priv->error) { + g_simple_async_result_take_error(res, self->priv->error); g_simple_async_result_set_op_res_gboolean(res, FALSE); } else { g_simple_async_result_set_op_res_gboolean(res, TRUE); if (spice_util_get_debug()) { gint64 now = g_get_monotonic_time(); - gchar *basename = g_file_get_basename(task->file); - double seconds = (double) (now - task->start_time) / G_TIME_SPAN_SECOND; - gchar *file_size_str = g_format_size(task->file_size); - gchar *transfer_speed_str = g_format_size(task->file_size / seconds); + 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(task->read_bytes == task->file_size); + 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); @@ -1757,21 +1807,21 @@ static void file_xfer_close_cb(GObject *object, g_simple_async_result_complete_in_idle(res); g_object_unref(res); - file_xfer_task_free(task); + g_object_unref(self); } static void file_xfer_data_flushed_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { - SpiceFileXferTask *task = user_data; + SpiceFileTransferTask *self = user_data; SpiceMainChannel *channel = (SpiceMainChannel *)source_object; GError *error = NULL; - task->pending = FALSE; + self->priv->pending = FALSE; file_xfer_flush_finish(channel, res, &error); - if (error || task->error) { - file_xfer_completed(task, error); + if (error || self->priv->error) { + spice_file_transfer_task_completed(self, error); return; } @@ -1779,19 +1829,19 @@ static void file_xfer_data_flushed_cb(GObject *source_object, const GTimeSpan interval = 20 * G_TIME_SPAN_SECOND; gint64 now = g_get_monotonic_time(); - if (interval < now - task->last_update) { - gchar *basename = g_file_get_basename(task->file); - task->last_update = now; + 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 * task->read_bytes / task->file_size, basename); + 100.0 * self->priv->read_bytes / self->priv->file_size, basename); g_free(basename); } } - if (task->progress_callback) { + if (self->priv->progress_callback) { goffset read = 0; goffset total = 0; - SpiceMainChannel *main_channel = task->channel; + SpiceMainChannel *main_channel = self->priv->channel; GHashTableIter iter; gpointer key, value; @@ -1800,28 +1850,28 @@ static void file_xfer_data_flushed_cb(GObject *source_object, * current transfers */ g_hash_table_iter_init(&iter, main_channel->priv->file_xfer_tasks); while (g_hash_table_iter_next(&iter, &key, &value)) { - SpiceFileXferTask *t = (SpiceFileXferTask *)value; - read += t->read_bytes; - total += t->file_size; + SpiceFileTransferTask *t = (SpiceFileTransferTask *)value; + read += t->priv->read_bytes; + total += t->priv->file_size; } - task->progress_callback(read, total, task->progress_callback_data); + self->priv->progress_callback(read, total, self->priv->progress_callback_data); } /* Read more data */ - file_xfer_continue_read(task); + file_xfer_continue_read(self); } -static void file_xfer_queue(SpiceFileXferTask *task, int data_size) +static void file_xfer_queue(SpiceFileTransferTask *self, int data_size) { VDAgentFileXferDataMessage msg; - SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(task->channel); + SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(self->priv->channel); - msg.id = task->id; + msg.id = self->priv->id; msg.size = data_size; agent_msg_queue_many(channel, VD_AGENT_FILE_XFER_DATA, &msg, sizeof(msg), - task->buffer, data_size, NULL); + self->priv->buffer, data_size, NULL); spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE); } @@ -1830,52 +1880,53 @@ static void file_xfer_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { - SpiceFileXferTask *task = user_data; - SpiceMainChannel *channel = task->channel; + SpiceFileTransferTask *self = user_data; + SpiceMainChannel *channel = self->priv->channel; gssize count; GError *error = NULL; - task->pending = FALSE; - count = g_input_stream_read_finish(G_INPUT_STREAM(task->file_stream), + 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 (task->error) { - file_xfer_completed(task, error); + if (self->priv->error) { + spice_file_transfer_task_completed(self, error); return; } - if (count > 0 || task->file_size == 0) { - task->read_bytes += count; - file_xfer_queue(task, count); + if (count > 0 || self->priv->file_size == 0) { + self->priv->read_bytes += count; + g_object_notify(G_OBJECT(self), "progress"); + file_xfer_queue(self, count); if (count == 0) return; - file_xfer_flush_async(channel, task->cancellable, - file_xfer_data_flushed_cb, task); - task->pending = TRUE; + file_xfer_flush_async(channel, self->priv->cancellable, + file_xfer_data_flushed_cb, self); + self->priv->pending = TRUE; } else if (error) { VDAgentFileXferStatusMessage msg = { - .id = task->id, + .id = self->priv->id, .result = VD_AGENT_FILE_XFER_STATUS_ERROR, }; - agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_STATUS, + agent_msg_queue_many(self->priv->channel, VD_AGENT_FILE_XFER_STATUS, &msg, sizeof(msg), NULL); - spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE); - file_xfer_completed(task, 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(SpiceFileXferTask *task) +static void file_xfer_continue_read(SpiceFileTransferTask *self) { - g_input_stream_read_async(G_INPUT_STREAM(task->file_stream), - task->buffer, + g_input_stream_read_async(G_INPUT_STREAM(self->priv->file_stream), + self->priv->buffer, FILE_XFER_CHUNK_SIZE, G_PRIORITY_DEFAULT, - task->cancellable, + self->priv->cancellable, file_xfer_read_cb, - task); - task->pending = TRUE; + self); + self->priv->pending = TRUE; } /* coroutine context */ @@ -1883,10 +1934,9 @@ static void file_xfer_handle_status(SpiceMainChannel *channel, VDAgentFileXferStatusMessage *msg) { SpiceMainChannelPrivate *c = channel->priv; - SpiceFileXferTask *task; + SpiceFileTransferTask *task; GError *error = NULL; - task = g_hash_table_lookup(c->file_xfer_tasks, GUINT_TO_POINTER(msg->id)); if (task == NULL) { SPICE_DEBUG("cannot find task %d", msg->id); @@ -1897,7 +1947,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel, switch (msg->result) { case VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: - if (task->pending) { + if (task->priv->pending) { error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, "transfer received CAN_SEND_DATA in pending state"); break; @@ -1913,7 +1963,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel, "some errors occurred in the spice agent"); break; case VD_AGENT_FILE_XFER_STATUS_SUCCESS: - if (task->pending) + if (task->priv->pending) error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED, "transfer received success in pending state"); break; @@ -1924,7 +1974,7 @@ static void file_xfer_handle_status(SpiceMainChannel *channel, break; } - file_xfer_completed(task, error); + spice_file_transfer_task_completed(task, error); } /* any context: the message is not flushed immediately, @@ -2868,33 +2918,36 @@ void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean spice_main_update_display_enabled(channel, id, enabled, TRUE); } -static void file_xfer_completed(SpiceFileXferTask *task, GError *error) +static void spice_file_transfer_task_completed(SpiceFileTransferTask *self, + GError *error) { /* In case of multiple errors we only report the first error */ - if (task->error) + if (self->priv->error) g_clear_error(&error); if (error) { SPICE_DEBUG("File %s xfer failed: %s", - g_file_get_path(task->file), error->message); - task->error = error; + g_file_get_path(self->priv->file), error->message); + self->priv->error = error; } - if (task->pending) + if (self->priv->pending) return; - if (!task->file_stream) { - file_xfer_close_cb(NULL, NULL, task); + if (!self->priv->file_stream) { + file_xfer_close_cb(NULL, NULL, self); return; } - g_input_stream_close_async(G_INPUT_STREAM(task->file_stream), + g_input_stream_close_async(G_INPUT_STREAM(self->priv->file_stream), G_PRIORITY_DEFAULT, - task->cancellable, + self->priv->cancellable, file_xfer_close_cb, - task); - task->pending = TRUE; + self); + self->priv->pending = TRUE; + g_signal_emit(self, task_signals[SIGNAL_FINISHED], 0, error); } + static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data) { GFileInfo *info; @@ -2905,15 +2958,16 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da VDAgentFileXferStartMessage msg; gsize /*msg_size*/ data_len; gchar *string; - SpiceFileXferTask *task = (SpiceFileXferTask *)data; + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); - task->pending = FALSE; + self->priv->pending = FALSE; info = g_file_query_info_finish(file, res, &error); - if (error || task->error) + if (error || self->priv->error) goto failed; - task->file_size = + self->priv->file_size = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); + g_object_notify(G_OBJECT(self), "progress"); keyfile = g_key_file_new(); /* File name */ @@ -2921,7 +2975,7 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da g_key_file_set_string(keyfile, "vdagent-file-xfer", "name", basename); g_free(basename); /* File size */ - g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size", task->file_size); + g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size", self->priv->file_size); /* Save keyfile content to memory. TODO: more file attributions need to be sent to guest */ @@ -2931,41 +2985,45 @@ static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer da goto failed; /* Create file-xfer start message */ - msg.id = task->id; - agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_START, + msg.id = self->priv->id; + agent_msg_queue_many(self->priv->channel, VD_AGENT_FILE_XFER_START, &msg, sizeof(msg), string, data_len + 1, NULL); g_free(string); - spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE); + spice_channel_wakeup(SPICE_CHANNEL(self->priv->channel), FALSE); return; failed: - file_xfer_completed(task, error); + 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); - SpiceFileXferTask *task = (SpiceFileXferTask *)data; + SpiceFileTransferTask *self = SPICE_FILE_TRANSFER_TASK(data); GError *error = NULL; - task->pending = FALSE; - task->file_stream = g_file_read_finish(file, res, &error); - if (error || task->error) { - file_xfer_completed(task, error); + 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(task->file, + g_file_query_info_async(self->priv->file, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, - task->cancellable, + self->priv->cancellable, file_xfer_info_async_cb, - task); - task->pending = TRUE; + self); + self->priv->pending = TRUE; } +static SpiceFileTransferTask *spice_file_transfer_task_new(SpiceMainChannel *channel, + GFile *file, + GCancellable *cancellable); + static void file_xfer_send_start_msg_async(SpiceMainChannel *channel, GFile **files, GFileCopyFlags flags, @@ -2976,39 +3034,41 @@ static void file_xfer_send_start_msg_async(SpiceMainChannel *channel, gpointer user_data) { SpiceMainChannelPrivate *c = channel->priv; - SpiceFileXferTask *task; - static uint32_t xfer_id; /* Used to identify task id */ + SpiceFileTransferTask *task; gint i; for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) { - task = g_malloc0(sizeof(SpiceFileXferTask)); - task->id = ++xfer_id; - task->channel = g_object_ref(channel); - task->file = g_object_ref(files[i]); - task->flags = flags; - task->cancellable = cancellable; - task->progress_callback = progress_callback; - task->progress_callback_data = progress_callback_data; - task->callback = callback; - task->user_data = user_data; - - if (spice_util_get_debug()) { - gchar *basename = g_file_get_basename(task->file); - task->start_time = g_get_monotonic_time(); - task->last_update = task->start_time; - - SPICE_DEBUG("transfer of file %s has started", basename); - g_free(basename); - } - CHANNEL_DEBUG(task->channel, "Insert a xfer task:%d to task list", task->id); - g_hash_table_insert(c->file_xfer_tasks, GUINT_TO_POINTER(task->id), task); + 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->progress_callback = progress_callback; + task->priv->progress_callback_data = progress_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); + g_hash_table_insert(c->file_xfer_tasks, + GUINT_TO_POINTER(task->priv->id), + task); + g_signal_emit(channel, signals[SPICE_MAIN_NEW_FILE_TRANSFER], 0, task); g_file_read_async(files[i], G_PRIORITY_DEFAULT, cancellable, file_xfer_read_async_cb, task); - task->pending = TRUE; + task->priv->pending = TRUE; + + /* if we created a per-task cancellable above, free it */ + if (!cancellable) + g_object_unref(task_cancellable); } } @@ -3038,7 +3098,8 @@ static void file_xfer_send_start_msg_async(SpiceMainChannel *channel, * was broken since it only provided status for a single file transfer, but did * not provide a way to determine which file it referred to. In release 0.31, * this behavior was changed so that progress_callback provides the status of - * all ongoing file transfers. + * all ongoing file transfers. If you need to monitor the status of individual + * files, please connect to the #SpiceMainChannel::new-file-transfer signal. * * When the operation is finished, callback will be called. You can then call * spice_main_file_copy_finish() to get the result of the operation. @@ -3108,3 +3169,252 @@ gboolean spice_main_file_copy_finish(SpiceMainChannel *channel, return g_simple_async_result_get_op_res_gboolean(simple); } + + + +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 + **/ + 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 + **/ + 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 + **/ + 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 + **/ + 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. + **/ + 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::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. + **/ + 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); +} + +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 + **/ +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. + **/ +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 + **/ +char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self) +{ + return g_file_get_basename(self->priv->file); +} diff --git a/src/map-file b/src/map-file index a9abc61..92a9883 100644 --- a/src/map-file +++ b/src/map-file @@ -33,6 +33,11 @@ spice_display_new_with_monitor; spice_display_paste_from_guest; spice_display_send_keys; spice_display_set_grab_keys; +spice_file_transfer_task_cancel; +spice_file_transfer_task_get_filename; +spice_file_transfer_task_get_finished; +spice_file_transfer_task_get_progress; +spice_file_transfer_task_get_type; spice_get_option_group; spice_grab_sequence_as_string; spice_grab_sequence_copy; diff --git a/src/spice-client.h b/src/spice-client.h index aaa6775..b794472 100644 --- a/src/spice-client.h +++ b/src/spice-client.h @@ -50,6 +50,7 @@ #include "smartcard-manager.h" #include "usb-device-manager.h" #include "spice-audio.h" +#include "spice-file-transfer-task.h" G_BEGIN_DECLS diff --git a/src/spice-file-transfer-task.h b/src/spice-file-transfer-task.h new file mode 100644 index 0000000..b97d107 --- /dev/null +++ b/src/spice-file-transfer-task.h @@ -0,0 +1,68 @@ +/* + Copyright (C) 2010-2015 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_H__ +#define __SPICE_FILE_TRANSFER_TASK_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define SPICE_TYPE_FILE_TRANSFER_TASK spice_file_transfer_task_get_type() + +#define SPICE_FILE_TRANSFER_TASK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTask)) +#define SPICE_FILE_TRANSFER_TASK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskClass)) +#define SPICE_IS_FILE_TRANSFER_TASK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_FILE_TRANSFER_TASK)) +#define SPICE_IS_FILE_TRANSFER_TASK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_FILE_TRANSFER_TASK)) +#define SPICE_FILE_TRANSFER_TASK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_FILE_TRANSFER_TASK, SpiceFileTransferTaskClass)) + +typedef struct _SpiceFileTransferTask SpiceFileTransferTask; +typedef struct _SpiceFileTransferTaskClass SpiceFileTransferTaskClass; +typedef struct _SpiceFileTransferTaskPrivate SpiceFileTransferTaskPrivate; + +/** + * SpiceFileTransferTask: + * + * The #FileTransferTask struct is opaque and should not be accessed directly. + */ +struct _SpiceFileTransferTask +{ + GObject parent; + + SpiceFileTransferTaskPrivate *priv; +}; + +/** + * SpiceFileTransferTaskClass: + * @parent_class: Parent class. + * + * Class structure for #SpiceFileTransferTask. + */ +struct _SpiceFileTransferTaskClass +{ + GObjectClass parent_class; +}; + +GType spice_file_transfer_task_get_type(void) G_GNUC_CONST; + +char* spice_file_transfer_task_get_filename(SpiceFileTransferTask *self); +void spice_file_transfer_task_cancel(SpiceFileTransferTask *self); +double spice_file_transfer_task_get_progress(SpiceFileTransferTask *self); + +G_END_DECLS + +#endif /* __SPICE_FILE_TRANSFER_TASK_H__ */ diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file index 1d62716..3817a46 100644 --- a/src/spice-glib-sym-file +++ b/src/spice-glib-sym-file @@ -20,6 +20,11 @@ spice_client_error_quark spice_cursor_channel_get_type spice_display_channel_get_type spice_display_get_primary +spice_file_transfer_task_cancel +spice_file_transfer_task_get_filename +spice_file_transfer_task_get_finished +spice_file_transfer_task_get_progress +spice_file_transfer_task_get_type spice_get_option_group spice_g_signal_connect_object spice_inputs_button_press diff --git a/src/spicy.c b/src/spicy.c index f48a220..4de56d9 100644 --- a/src/spicy.c +++ b/src/spicy.c @@ -91,6 +91,7 @@ G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT); #define CHANNELID_MAX 4 #define MONITORID_MAX 4 + // FIXME: turn this into an object, get rid of fixed wins array, use // signals to replace the various callback that iterate over wins array struct spice_connection { @@ -104,6 +105,10 @@ struct spice_connection { gboolean agent_connected; int channels; int disconnecting; + + /* key: SpiceFileTransferTask, value: TransferTaskWidgets */ + GHashTable *transfers; + GtkWidget *transfer_dialog; }; static spice_connection *connection_new(void); @@ -1386,6 +1391,148 @@ static void port_data(SpicePortChannel *port, } } +typedef struct { + GtkWidget *vbox; + GtkWidget *hbox; + GtkWidget *progress; + GtkWidget *label; + GtkWidget *cancel; +} TransferTaskWidgets; + +static void transfer_update_progress(GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + spice_connection *conn = user_data; + TransferTaskWidgets *widgets = g_hash_table_lookup(conn->transfers, object); + g_return_if_fail(widgets); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress), + spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(object))); +} + +static void transfer_task_finished(SpiceFileTransferTask *task, GError *error, spice_connection *conn) +{ + if (error) + g_warning("%s", error->message); + g_hash_table_remove(conn->transfers, task); + if (!g_hash_table_size(conn->transfers)) + gtk_widget_hide(conn->transfer_dialog); +} + +static void dialog_response_cb(GtkDialog *dialog, + gint response_id, + gpointer user_data) +{ + spice_connection *conn = user_data; + g_print("Reponse: %i\n", response_id); + + if (response_id == GTK_RESPONSE_CANCEL) { + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, conn->transfers); + while (g_hash_table_iter_next(&iter, &key, &value)) { + SpiceFileTransferTask *task = key; + spice_file_transfer_task_cancel(task); + } + } +} + +void task_cancel_cb(GtkButton *button, + gpointer user_data) +{ + SpiceFileTransferTask *task = SPICE_FILE_TRANSFER_TASK(user_data); + spice_file_transfer_task_cancel(task); +} + +TransferTaskWidgets *transfer_task_widgets_new(SpiceFileTransferTask *task) +{ + TransferTaskWidgets *widgets = g_new0(TransferTaskWidgets, 1); + +#if GTK_CHECK_VERSION(3,0,0) + widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + widgets->cancel = gtk_button_new_from_icon_name(GTK_STOCK_CANCEL, + GTK_ICON_SIZE_SMALL_TOOLBAR); +#else + widgets->vbox = gtk_vbox_new(FALSE, 0); + widgets->hbox = gtk_hbox_new(FALSE, 6); + widgets->cancel = gtk_button_new_from_stock(GTK_STOCK_CANCEL); +#endif + + widgets->progress = gtk_progress_bar_new(); + widgets->label = gtk_label_new(spice_file_transfer_task_get_filename(task)); + +#if GTK_CHECK_VERSION(3,0,0) + gtk_widget_set_halign(widgets->label, GTK_ALIGN_START); + gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE); + gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(widgets->progress, TRUE); + gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(widgets->progress, FALSE); +#endif + + gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress, + TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel, + FALSE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label, + TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox, + TRUE, TRUE, 0); + + g_signal_connect(widgets->cancel, "clicked", + G_CALLBACK(task_cancel_cb), task); + + gtk_widget_show_all(widgets->vbox); + + return widgets; +} + +void transfer_task_widgets_free(TransferTaskWidgets *widgets) +{ + /* child widgets will be destroyed automatically */ + gtk_widget_destroy(widgets->vbox); + g_free(widgets); +} + +static void spice_connection_add_task(spice_connection *conn, SpiceFileTransferTask *task) +{ + TransferTaskWidgets *widgets; + GtkWidget *content = NULL; + + g_signal_connect(task, "notify::progress", + G_CALLBACK(transfer_update_progress), conn); + g_signal_connect(task, "finished", + G_CALLBACK(transfer_task_finished), conn); + if (!conn->transfer_dialog) { + conn->transfer_dialog = gtk_dialog_new_with_buttons("File Transfers", + GTK_WINDOW(conn->wins[0]->toplevel), 0, + "Cancel", GTK_RESPONSE_CANCEL, NULL); + gtk_dialog_set_default_response(GTK_DIALOG(conn->transfer_dialog), + GTK_RESPONSE_CANCEL); + gtk_window_set_resizable(GTK_WINDOW(conn->transfer_dialog), FALSE); + g_signal_connect(conn->transfer_dialog, "response", + G_CALLBACK(dialog_response_cb), conn); + } + gtk_widget_show(conn->transfer_dialog); + content = gtk_dialog_get_content_area(GTK_DIALOG(conn->transfer_dialog)); + gtk_container_set_border_width(GTK_CONTAINER(content), 12); + + widgets = transfer_task_widgets_new(task); + g_hash_table_insert(conn->transfers, g_object_ref(task), widgets); + gtk_box_pack_start(GTK_BOX(content), + widgets->vbox, TRUE, TRUE, 6); +} + +static void new_file_transfer(SpiceMainChannel *main, SpiceFileTransferTask *task, gpointer user_data) +{ + spice_connection *conn = user_data; + g_debug("new file transfer task"); + spice_connection_add_task(conn, task); +} + static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) { spice_connection *conn = data; @@ -1404,6 +1551,8 @@ static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) G_CALLBACK(main_mouse_update), conn); g_signal_connect(channel, "main-agent-update", G_CALLBACK(main_agent_update), conn); + g_signal_connect(channel, "new-file-transfer", + G_CALLBACK(new_file_transfer), conn); main_mouse_update(channel, conn); main_agent_update(channel, conn); } @@ -1515,6 +1664,9 @@ static spice_connection *connection_new(void) G_CALLBACK(usb_connect_failed), NULL); } + conn->transfers = g_hash_table_new_full(g_direct_hash, g_direct_equal, + g_object_unref, + (GDestroyNotify)transfer_task_widgets_free); connections++; SPICE_DEBUG("%s (%d)", __FUNCTION__, connections); return conn; |