<div dir="ltr"><br><div class="gmail_extra">Hi!<br></div><div class="gmail_extra"><br><div class="gmail_quote">On Fri, Dec 28, 2012 at 2:33 PM, Dunrong Huang <span dir="ltr"><<a href="mailto:riegamaths@gmail.com" target="_blank">riegamaths@gmail.com</a>></span> wrote:<br>
<blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">Agent channel is a flow-control channel. That means before<br>
we send agent data to server, we must obtain tokens distributed<br>
from spice server, if we do not do that, spice server will get error,<br>
or at least, the data will be discarded.<br>
<br>
Other type of agent data will be cached to agent_msg_queue if there<br>
are no more tokens. But for file-xfer data, if we cache too much of<br>
those data, our memory will be exhausted pretty quickly if file is<br>
too big.<br>
<br>
We also should make other agent data(clipboard, mouse, ...) get<br>
through when file-xfer data are sending.<br>
<br>
So, for the reason of above, we can not fill file-xfer data to agent queue<br>
too quickly, we must consider the tokens, and other messages.<br>
<br>
Marc-André suggested me to call spice_channel_flush_async() and wait the<br>
queued data to be sent, but the API does not consider the available<br>
tokens, so I use a new algorithm/API(file_xfer_flush_async) based on<br>
spice_channel_flush_async() to send file-xfer data.<br>
<br></blockquote><div><br></div><div>Right<br></div><div><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
 gtk/channel-main.c      | 476 ++++++++++++++++++++++++++++++++++++++++++++++++<br>
 gtk/channel-main.h      |   8 +<br>
 gtk/map-file            |   1 +<br>
 gtk/spice-glib-sym-file |   1 +<br>
 4 files changed, 486 insertions(+)<br>
<br>
diff --git a/gtk/channel-main.c b/gtk/channel-main.c<br>
index 6b9ba8d..b1496bd 100644<br>
--- a/gtk/channel-main.c<br>
+++ b/gtk/channel-main.c<br>
@@ -18,6 +18,7 @@<br>
 #include <math.h><br>
 #include <spice/vd_agent.h><br>
 #include <common/rect.h><br>
+#include <glib/gstdio.h><br>
<br>
 #include "glib-compat.h"<br>
 #include "spice-client.h"<br>
@@ -51,6 +52,24 @@<br>
<br>
 typedef struct spice_migrate spice_migrate;<br>
<br>
+#define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32)<br>
+typedef struct SpiceFileXferTask {<br>
+    uint32_t                       id;<br>
+    uint32_t                       group_id;<br>
+    GFile                          *file;<br>
+    SpiceMainChannel               *channel;<br>
+    GFileInputStream               *file_stream;<br>
+    GFileCopyFlags                 flags;<br>
+    GCancellable                   *cancellable;<br>
+    GFileProgressCallback          progress_callback;<br>
+    gpointer                       progress_callback_data;<br>
+    GAsyncReadyCallback            callback;<br>
+    gpointer                       user_data;<br>
+    char                           buffer[FILE_XFER_CHUNK_SIZE];<br>
+    uint64_t                       read_bytes;<br>
+    uint64_t                       file_size;<br>
+} SpiceFileXferTask;<br>
+<br>
 struct _SpiceMainChannelPrivate  {<br>
     enum SpiceMouseMode         mouse_mode;<br>
     bool                        agent_connected;<br>
@@ -79,6 +98,8 @@ struct _SpiceMainChannelPrivate  {<br>
     } display[MAX_DISPLAY];<br>
     gint                        timer_id;<br>
     GQueue                      *agent_msg_queue;<br>
+    GList                       *file_xfer_task_list;<br>
+    GSList                      *flushing;<br>
<br>
     guint                       switch_host_delayed_id;<br>
     guint                       migrate_delayed_id;<br>
@@ -802,6 +823,63 @@ static void agent_free_msg_queue(SpiceMainChannel *channel)<br>
     c->agent_msg_queue = NULL;<br>
 }<br>
<br>
+/* Here, flushing algorithm is stolen from spice-channel.c */<br>
+static void<br>
+file_xfer_flushed(SpiceMainChannel *channel, gboolean success)<br>
+{<br>
+    SpiceMainChannelPrivate *c = channel->priv;<br>
+    GSList *l;<br>
+<br>
+    for (l = c->flushing; l != NULL; l = l->next) {<br>
+        GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(l->data);<br>
+        g_simple_async_result_set_op_res_gboolean(result, success);<br>
+        g_simple_async_result_complete_in_idle(result);<br>
+    }<br>
+<br>
+    g_slist_free_full(c->flushing, g_object_unref);<br>
+    c->flushing = NULL;<br>
+}<br>
+<br>
+static void<br>
+file_xfer_flush_async(SpiceMainChannel *channel, GCancellable *cancellable,<br>
+                      GAsyncReadyCallback callback, gpointer user_data)<br>
+{<br>
+    GSimpleAsyncResult *simple;<br>
+    SpiceMainChannelPrivate *c = channel->priv;<br>
+    gboolean was_empty;<br>
+<br>
+    simple = g_simple_async_result_new(G_OBJECT(channel), callback, user_data,<br>
+                                       file_xfer_flush_async);<br>
+<br>
+    was_empty = g_queue_is_empty(c->agent_msg_queue);<br>
+    if (was_empty) {<br>
+        g_simple_async_result_set_op_res_gboolean(simple, TRUE);<br>
+        g_simple_async_result_complete_in_idle(simple);<br>
+        return;<br>
+    }<br>
+<br>
+    c->flushing = g_slist_append(c->flushing, simple);<br>
+}<br>
+<br>
+static gboolean<br>
+file_xfer_flush_finish(SpiceMainChannel *channel, GAsyncResult *result,<br>
+                       GError **error)<br>
+{<br>
+    GSimpleAsyncResult *simple;<br>
+<br>
+    simple = (GSimpleAsyncResult *)result;<br>
+<br>
+    if (g_simple_async_result_propagate_error(simple, error)) {<br>
+        return -1;<br>
+    }<br>
+<br>
+    g_return_val_if_fail(g_simple_async_result_is_valid(result,<br>
+        G_OBJECT(channel), file_xfer_flush_async), FALSE);<br>
+<br>
+    CHANNEL_DEBUG(channel, "flushed finished!");<br>
+    return g_simple_async_result_get_op_res_gboolean(simple);<br>
+}<br>
+<br>
 /* coroutine context */<br>
 static void agent_send_msg_queue(SpiceMainChannel *channel)<br>
 {<br>
@@ -814,6 +892,9 @@ static void agent_send_msg_queue(SpiceMainChannel *channel)<br>
         out = g_queue_pop_head(c->agent_msg_queue);<br>
         spice_msg_out_send_internal(out);<br>
     }<br>
+    if (g_queue_is_empty(c->agent_msg_queue) && c->flushing != NULL) {<br>
+        file_xfer_flushed(channel, TRUE);<br>
+    }<br>
 }<br>
<br>
 /* any context: the message is not flushed immediately,<br>
@@ -1384,6 +1465,203 @@ static void main_handle_agent_disconnected(SpiceChannel *channel, SpiceMsgIn *in<br>
     agent_stopped(SPICE_MAIN_CHANNEL(channel));<br>
 }<br>
<br>
+static gint file_xfer_task_find(gconstpointer a, gconstpointer b)<br>
+{<br>
+    SpiceFileXferTask *task = (SpiceFileXferTask *)a;<br>
+    uint32_t id = *(uint32_t *)b;<br>
+<br>
+    if (task->id == id) {<br>
+        return 0;<br>
+    }<br>
+<br>
+    return 1;<br>
+}<br>
+<br>
+static void file_read_cb(GObject *source_object,<br>
+                         GAsyncResult *res,<br>
+                         gpointer user_data);<br>
+<br>
+static gboolean report_progress(gpointer user_data)<br>
+{<br>
+    SpiceFileXferTask *task = user_data;<br>
+    SpiceMainChannelPrivate *c = task->channel->priv;<br>
+<br>
+    if (task->progress_callback) {<br>
+        uint64_t all_read_bytes = 0, all_bytes = 0;<br>
+        GList *it;<br>
+        for (it = g_list_first(c->file_xfer_task_list);<br>
+             it != NULL; it = g_list_next(it)) {<br>
+            SpiceFileXferTask *t;<br>
+            t = it->data;<br>
+            /* Calculate all remain bytes through group id, NB: we dont<br>
+             * consider the task that has been finished */<br>
+            if (t->group_id == task->id) {<br>
+                all_read_bytes += t->read_bytes;<br>
+                all_bytes += t->file_size;<br>
+            }<br>
+        }<br>
+        task->progress_callback(all_read_bytes, all_bytes,<br>
+                                task->progress_callback_data);<br>
+    }<br>
+<br>
+    return FALSE;<br>
+}<br>
+<br>
+static void data_flushed_cb(GObject *source_object,<br>
+                            GAsyncResult *res,<br>
+                            gpointer user_data)<br>
+{<br>
+    SpiceFileXferTask *task = user_data;<br>
+    SpiceMainChannel *channel = (SpiceMainChannel *)source_object;<br>
+    GError *error = NULL;<br>
+<br>
+    file_xfer_flush_finish(channel, res, &error);<br></blockquote><div><br></div><div>Even if the function currently doesn't report error, you should treat error case at least with a g_warning().<br> <br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+    /* Report progress */<br>
+    report_progress(task);<br>
+<br>
+    /* Read more data */<br>
+    g_input_stream_read_async(G_INPUT_STREAM(task->file_stream),<br>
+                              task->buffer,<br>
+                              FILE_XFER_CHUNK_SIZE,<br>
+                              G_PRIORITY_DEFAULT,<br>
+                              task->cancellable,<br>
+                              file_read_cb,<br>
+                              task);<br>
+}<br>
+<br>
+static void<br>
+file_xfer_queue(SpiceFileXferTask *task, int data_size)<br>
+{<br>
+    VDAgentFileXferDataMessage *msg;<br>
+    SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(task->channel);<br>
+<br>
+    msg = g_alloca(sizeof(VDAgentFileXferDataMessage));<br>
+    msg->id = task->id;<br>
+    msg->size = data_size;<br>
+    agent_msg_queue_many(channel, VD_AGENT_FILE_XFER_DATA, msg,<br>
+                         sizeof(VDAgentFileXferDataMessage), task->buffer,<br>
+                         data_size, NULL);<br>
+    spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);<br>
+}<br>
+<br>
+static void report_finish(SpiceFileXferTask *task)<br>
+{<br>
+    if (task->callback) {<br>
+        GSimpleAsyncResult *res;<br>
+        res = g_simple_async_result_new(G_OBJECT(task->file), task->callback,<br>
+                                        task->user_data, report_finish);<br>
+        g_simple_async_result_set_op_res_gboolean(res, TRUE);<br>
+        g_simple_async_result_complete_in_idle(res);<br>
+        g_object_unref(res);<br>
+    }<br>
+}<br>
+<br>
+/* main context */<br>
+static void<br>
+file_close_cb(GObject      *object,<br>
+              GAsyncResult *res,<br>
+              gpointer      user_data)<br>
+{<br>
+    SpiceFileXferTask *task = user_data;<br>
+    GInputStream *stream = G_INPUT_STREAM(object);<br>
+    SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(task->channel)->priv;<br>
+    GError *error = NULL;<br>
+<br>
+    g_input_stream_close_finish(stream, res, &error);<br>
+    if (error) {<br>
+        SPICE_DEBUG("close file error: %s", error->message);<br>
+        g_clear_error(&error);<br>
+    }<br>
+<br>
+    c->file_xfer_task_list = g_list_remove(c->file_xfer_task_list, task);<br>
+<br>
+    /* If all tasks have been finished, notify to user */<br>
+    if (g_list_length(c->file_xfer_task_list) == 0) {<br>
+        report_finish(task);<br>
+    }<br>
+    g_object_unref(task->file);<br>
+    g_object_unref(task->file_stream);<br>
+    g_free(task);<br>
+}<br>
+<br>
+/* main context */<br>
+static void file_read_cb(GObject *source_object,<br>
+                         GAsyncResult *res,<br>
+                         gpointer user_data)<br>
+{<br>
+    SpiceFileXferTask *task = user_data;<br>
+    SpiceMainChannel *channel = task->channel;<br>
+    gssize count;<br>
+    GError *error = NULL;<br>
+<br>
+    count = g_input_stream_read_finish(G_INPUT_STREAM(task->file_stream),<br>
+                                       res, &error);<br>
+    if (count > 0) {<br>
+        task->read_bytes += count;<br>
+        file_xfer_queue(task, count);<br>
+        file_xfer_flush_async(channel, task->cancellable,<br>
+                              data_flushed_cb, task);<br>
+    } else {<br>
+        g_input_stream_close_async(G_INPUT_STREAM(task->file_stream),<br>
+                                   G_PRIORITY_DEFAULT,<br>
+                                   task->cancellable,<br>
+                                   file_close_cb,<br>
+                                   task);<br></blockquote><div><br>You should report error to caller (probably with g_simple_async_report_gerror_in_idle)<br> <br></div><div>There will be some issues with the fact that there are multiple outstanding async in the background. The common pattern is to _always_ complete one async() call  with one result (succesful or error). There shouldn't be "lost" async, or you may "block" some client execution path.<br>
<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+    }<br>
+}<br>
+<br>
+/* coroutine context */<br>
+static void file_xfer_send_data_msg(SpiceMainChannel *channel, uint32_t id)<br>
+{<br>
+    SpiceMainChannelPrivate *c = channel->priv;<br>
+    GList *l;<br>
+    SpiceFileXferTask *task;<br>
+<br>
+    l = g_list_find_custom(c->file_xfer_task_list, &id,<br>
+                           file_xfer_task_find);<br>
+<br>
+    g_return_if_fail(l != NULL);<br>
+<br>
+    task = l->data;<br>
+    g_input_stream_read_async(G_INPUT_STREAM(task->file_stream),<br>
+                              task->buffer,<br>
+                              FILE_XFER_CHUNK_SIZE,<br>
+                              G_PRIORITY_DEFAULT,<br>
+                              task->cancellable,<br>
+                              file_read_cb,<br>
+                              task);<br>
+}<br>
+<br>
+/* coroutine context */<br>
+static void file_xfer_handle_status(SpiceMainChannel *channel,<br>
+                                    VDAgentFileXferStatusMessage *msg)<br>
+{<br>
+    SPICE_DEBUG("task %d received response %d", msg->id, msg->result);<br></blockquote><div><br></div><div> You could lookup the task only once here.<br><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+<br>
+    if (msg->result == VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA) {<br>
+        file_xfer_send_data_msg(channel, msg->id);<br></blockquote><div><br></div><div>and can call g_input_stream_read_async() directly here, or perhaps move the read_async() in a seperate function task_continue_read() which will be called also from data_flushed_cb()<br>
  <br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+    } else {<br></blockquote><div><br></div><div>Please check precisely the other msg->result values, and do a g_warn_if_reached() for unknown values.<br><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+        /* Error, remove this task */<br>
+        SpiceMainChannelPrivate *c = channel->priv;<br>
+        GList *l;<br>
+        SpiceFileXferTask *task;<br>
+<br>
+        l = g_list_find_custom(c->file_xfer_task_list, &msg->id,<br>
+                               file_xfer_task_find);<br>
+        g_return_if_fail(l != NULL);<br>
+<br>
+        task = l->data;<br>
+        SPICE_DEBUG("user removed task %d, result: %d", msg->id,<br>
+                    msg->result);<br></blockquote><div> <br></div>You should report error to caller (probably with g_simple_async_report_gerror_in_idle)<br><br><br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+        c->file_xfer_task_list = g_list_remove(c->file_xfer_task_list,<br>
+                                               task);<br>
+        g_object_unref(task->file);<br>
+        g_object_unref(task->file_stream);<br>
+        g_free(task); </blockquote><div><br></div><div>Those last 4 lines could probably be in a seperate function file_xfer_task_free ()<br><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+    }<br>
+}<br>
+<br>
 /* coroutine context */<br>
 static void main_agent_handle_msg(SpiceChannel *channel,<br>
                                   VDAgentMessage *msg, gpointer payload)<br>
@@ -1487,6 +1765,9 @@ static void main_agent_handle_msg(SpiceChannel *channel,<br>
                     reply->error == VD_AGENT_SUCCESS ? "success" : "error");<br>
         break;<br>
     }<br>
+    case VD_AGENT_FILE_XFER_STATUS:<br>
+        file_xfer_handle_status(SPICE_MAIN_CHANNEL(channel), payload);<br>
+        break;<br>
     default:<br>
         g_warning("unhandled agent message type: %u (%s), size %u",<br>
                   msg->type, NAME(agent_msg_types, msg->type), msg->size);<br>
@@ -1563,6 +1844,7 @@ static void main_handle_agent_token(SpiceChannel *channel, SpiceMsgIn *in)<br>
     SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;<br>
<br>
     c->agent_tokens += tokens->num_tokens;<br>
+<br>
     agent_send_msg_queue(SPICE_MAIN_CHANNEL(channel));<br>
 }<br>
<br>
@@ -2246,3 +2528,197 @@ void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean<br>
         c->display[id].enabled = enabled;<br>
     }<br>
 }<br>
+<br>
+static void<br>
+file_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data)<br>
+{<br>
+    GFileInfo *info;<br>
+    GFile *file = G_FILE(obj);<br>
+    GError *error = NULL;<br>
+    GKeyFile *keyfile = NULL;<br>
+    gchar *basename = NULL;<br>
+    VDAgentFileXferStartMessage *msg;<br>
+    gsize msg_size, data_len;<br>
+    gchar *string;<br>
+    SpiceFileXferTask *task = (SpiceFileXferTask *)data;<br>
+    SpiceMainChannelPrivate *c = task->channel->priv;<br>
+<br>
+    info = g_file_query_info_finish(file, res, &error);<br>
+    if (error) {<br>
+        SPICE_DEBUG("couldn't get size of file %s: %s",<br>
+                    g_file_get_path(file),<br>
+                    error->message);<br>
+        goto failed;<br>
+    }<br>
+    task->file_size = g_file_info_get_attribute_uint64(info,<br>
+                                        G_FILE_ATTRIBUTE_STANDARD_SIZE);<br>
+<br>
+    keyfile = g_key_file_new();<br>
+    if (keyfile == NULL) {<br>
+        SPICE_DEBUG("failed to create key file: %s", error->message);<br>
+        goto failed;<br>
+    }<br>
+<br>
+    /* File name */<br>
+    basename = g_file_get_basename(file);<br>
+    if (basename == NULL) {<br>
+        SPICE_DEBUG("failed to get file basename: %s", error->message);<br>
+        goto failed;<br>
+    }<br>
+    g_key_file_set_string(keyfile, "vdagent-file-xfer", "name", basename);<br>
+    g_free(basename);<br>
+<br>
+    /* File size */<br>
+    g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size",<br>
+                          task->file_size);<br>
+<br>
+    /* Save keyfile content to memory. TODO: more file attributions<br>
+       need to be sent to guest */<br>
+    string = g_key_file_to_data(keyfile, &data_len, &error);<br>
+    g_key_file_free(keyfile);<br>
+    if (error) {<br>
+        goto failed;<br>
+    }<br>
+<br>
+    /* Create file-xfer start message */<br>
+    msg_size = sizeof(VDAgentFileXferStartMessage) + data_len + 1;<br>
+    msg = g_malloc0(msg_size);<br></blockquote><div><br>This allocation and copy seems unnecessary, can you use "string" directly?<br><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+    msg->id = task->id;<br>
+    memcpy(msg->data, string, data_len + 1);<br>
+    g_free(string);<br>
+<br>
+    CHANNEL_DEBUG(task->channel, "Insert a xfer task:%d to task list",<br>
+                  task->id);<br>
+    c->file_xfer_task_list = g_list_append(c->file_xfer_task_list, task);<br>
+<br>
+    agent_msg_queue(task->channel, VD_AGENT_FILE_XFER_START, msg_size, msg);<br>
+    g_free(msg);<br>
+    spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE);<br>
+    return ;<br>
+<br>
+failed:<br>
+    g_clear_error(&error);<br>
+    g_object_unref(task->file);<br>
+    g_object_unref(task->file_stream);<br>
+    g_free(task);<br>
+}<br>
+<br>
+static void<br>
+read_async_cb(GObject *obj, GAsyncResult *res, gpointer data)<br>
+{<br>
+    GFile *file = G_FILE(obj);<br>
+    SpiceFileXferTask *task = (SpiceFileXferTask *)data;<br>
+    GError *error = NULL;<br>
+<br>
+    task->file_stream = g_file_read_finish(file, res, &error);<br>
+<br>
+    if (task->file_stream) {<br>
+        g_file_query_info_async(task->file,<br>
+                                G_FILE_ATTRIBUTE_STANDARD_SIZE,<br>
+                                G_FILE_QUERY_INFO_NONE,<br>
+                                G_PRIORITY_DEFAULT,<br>
+                                task->cancellable,<br>
+                                file_info_async_cb,<br>
+                                task);<br>
+    } else {<br>
+        SPICE_DEBUG("create file stream for %s error: %s",<br>
+                    g_file_get_path(file), error->message);<br>
+        g_clear_error(&error);<br>
+        g_object_unref(task->file);<br>
+        g_free(task);<br></blockquote><div><br></div><div>You should report error to caller (probably with g_simple_async_report_gerror_in_idle)<br><br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">

+    }<br>
+}<br>
+<br>
+static void<br>
+file_xfer_send_start_msg_async(SpiceMainChannel *channel,<br>
+                               GFile *file,<br>
+                               GFileCopyFlags flags,<br>
+                               GCancellable *cancellable,<br>
+                               GFileProgressCallback progress_callback,<br>
+                               gpointer progress_callback_data,<br>
+                               GAsyncReadyCallback callback,<br>
+                               gpointer user_data,<br>
+                               uint32_t group_id)<br>
+{<br>
+    SpiceFileXferTask *task;<br>
+    static uint32_t xfer_id;    /* Used to identify task id */<br>
+<br>
+    xfer_id = (xfer_id > UINT32_MAX) ? 0 : xfer_id;<br>
+<br>
+    task = spice_malloc0(sizeof(SpiceFileXferTask));<br>
+    task->id = ++xfer_id;<br>
+    task->group_id = group_id;<br>
+    task->channel = channel;<br>
+    task->file = g_object_ref(file);<br>
+    task->flags = flags;<br>
+    task->cancellable = cancellable;<br>
+    task->progress_callback = progress_callback;<br>
+    task->progress_callback_data = progress_callback_data;<br>
+    task->callback = callback;<br>
+    task->user_data = user_data;<br>
+<br>
+    g_file_read_async(file,<br>
+                      G_PRIORITY_DEFAULT,<br>
+                      cancellable,<br>
+                      read_async_cb,<br>
+                      task);<br>
+<br>
+}<br>
+<br>
+/**<br>
+ * spice_main_file_copy_async:<br>
+ * @sources: #GFile to be transfer<br>
+ * @flags: set of #GFileCopyFlags<br>
+ * @cancellable: (allow-none): optional #GCancellable object, %NULL to ignore<br>
+ * @progress_callback: (allow-none) (scope call): function to callback with<br>
+ *     progress information, or %NULL if progress information is not needed<br>
+ * @progress_callback_data: (closure): user data to pass to @progress_callback<br>
+ * @error: #GError to set on error, or %NULL<br>
+ *<br>
+ * Copies the file @sources to guest<br>
+ *<br>
+ * If @cancellable is not %NULL, then the operation can be cancelled by<br>
+ * triggering the cancellable object from another thread. If the operation<br>
+ * was cancelled, the error %G_IO_ERROR_CANCELLED will be returned.<br>
+ *<br>
+ * If @progress_callback is not %NULL, then the operation can be monitored by<br>
+ * setting this to a #GFileProgressCallback function. @progress_callback_data<br>
+ * will be passed to this function. It is guaranteed that this callback will<br>
+ * be called after all data has been transferred with the total number of bytes<br>
+ * copied during the operation.<br>
+ *<br>
+ * When the operation is finished, callback will be called.<br>
+ *<br>
+ **/<br>
+void spice_main_file_copy_async(SpiceMainChannel *channel,<br>
+                                GFile **sources,<br>
+                                GFileCopyFlags flags,<br>
+                                GCancellable *cancellable,<br>
+                                GFileProgressCallback progress_callback,<br>
+                                gpointer progress_callback_data,<br>
+                                GAsyncReadyCallback callback,<br>
+                                gpointer user_data)<br>
+{<br>
+    int i = 0;<br>
+    static uint32_t xfer_group_id;<br>
+<br>
+    g_return_if_fail(channel != NULL);<br>
+    g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));<br>
+    g_return_if_fail(sources != NULL);<br>
+<br>
+    xfer_group_id++;<br>
+    xfer_group_id = (xfer_group_id > UINT32_MAX) ? 0 : xfer_group_id;<br>
+    while (sources[i]) {<br>
+        /* All tasks created from below function have same group id */<br></blockquote><div><br></div><div>I am worried by the server side handling of sharing the same group id for several requests. But I am okay with this communication pattern that can be later improved if needed.<br>
</div><div> </div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">
+        file_xfer_send_start_msg_async(channel,<br>
+                                       sources[i],<br>
+                                       flags,<br>
+                                       cancellable,<br>
+                                       progress_callback,<br>
+                                       progress_callback_data,<br>
+                                       callback,<br>
+                                       user_data,<br>
+                                       xfer_group_id);<br>
+        i++;<br>
+    }<br>
+}<br>
diff --git a/gtk/channel-main.h b/gtk/channel-main.h<br>
index 1a5ab54..d00490f 100644<br>
--- a/gtk/channel-main.h<br>
+++ b/gtk/channel-main.h<br>
@@ -78,6 +78,14 @@ void spice_main_clipboard_selection_notify(SpiceMainChannel *channel, guint sele<br>
 void spice_main_clipboard_selection_request(SpiceMainChannel *channel, guint selection, guint32 type);<br>
<br>
 gboolean spice_main_agent_test_capability(SpiceMainChannel *channel, guint32 cap);<br>
+void spice_main_file_copy_async(SpiceMainChannel *channel,<br>
+                                GFile **sources,<br>
+                                GFileCopyFlags flags,<br>
+                                GCancellable *cancellable,<br>
+                                GFileProgressCallback progress_callback,<br>
+                                gpointer progress_callback_data,<br>
+                                GAsyncReadyCallback callback,<br>
+                                gpointer user_data);<br>
<br>
 #ifndef SPICE_DISABLE_DEPRECATED<br>
 SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_grab)<br>
diff --git a/gtk/map-file b/gtk/map-file<br>
index 516764c..9988e7d 100644<br>
--- a/gtk/map-file<br>
+++ b/gtk/map-file<br>
@@ -55,6 +55,7 @@ spice_inputs_motion;<br>
 spice_inputs_position;<br>
 spice_inputs_set_key_locks;<br>
 spice_main_agent_test_capability;<br>
+spice_main_file_copy_async;<br>
 spice_main_channel_get_type;<br>
 spice_main_clipboard_grab;<br>
 spice_main_clipboard_notify;<br>
diff --git a/gtk/spice-glib-sym-file b/gtk/spice-glib-sym-file<br>
index 641ff4d..81fedd5 100644<br>
--- a/gtk/spice-glib-sym-file<br>
+++ b/gtk/spice-glib-sym-file<br>
@@ -31,6 +31,7 @@ spice_inputs_motion<br>
 spice_inputs_position<br>
 spice_inputs_set_key_locks<br>
 spice_main_agent_test_capability<br>
+spice_main_file_copy_async<br>
 spice_main_channel_get_type<br>
 spice_main_clipboard_grab<br>
 spice_main_clipboard_notify<br>
<span class=""><font color="#888888">--<br>
1.8.0<br>
<br>
_______________________________________________<br>
Spice-devel mailing list<br>
<a href="mailto:Spice-devel@lists.freedesktop.org">Spice-devel@lists.freedesktop.org</a><br>
<a href="http://lists.freedesktop.org/mailman/listinfo/spice-devel" target="_blank">http://lists.freedesktop.org/mailman/listinfo/spice-devel</a><br>
</font></span></blockquote></div><br> Thanks a lot for your work!<br clear="all"><br>-- <br>Marc-André Lureau
</div></div>