spice-gtk 源码分析——文件拖拽


spice-gtk目前还不完善,使用的时候需要分析源码自己改很多,当然分析源码的过程还是很费劲的,毕竟对gtk这东西不熟悉,下面是对【文件拖拽】的分析。对于客户端,文件拖拽走的是main channel,所以大部分代码在channel-main.c里,gtk用了大量的回调,但是下面还是按照基本顺序分析(只写了主干,细节自行结合源码看)


基本框架

根据spice-protocol,大致流程如下:

  • client发送VD_AGENT_FILE_XFER_START消息
  • agent确认磁盘容量,然后发送带有VD_AGENT_FILE_XFER_STATUS=VDAgentFileXferResult(见下)
  • 如果上一步OK,则client发送VD_AGENT_FILE_XFER_DATA消息,直到整个文件传送完成

数据结构

VD_AGENT_FILE_XFER_STATUS——传输前后一些状态

enum VDAgentFileXferResult { VD_AGENT_FILE_XFER_SUCCESS, VD_AGENT_FILE_XFER_DISK_FULL, VD_AGENT_FILE_XFER_CANCELLED };
typedef struct SPICE_ATTR_PACKED VDAgentFileXferStatusMessage{
    uint32_t id;
    uint32_t result;
} VDAgentFileXferStatusMessage;
GENT_FILE_XFER_START——文件基本属性信息

typedef struct SPICE_ATTR_PACKED VDAgentFileXferStartMessage{
    uint32_t id;
    uint64_t file_size;
    uint8_t file_name[0];
} VDAgentFileXferStartMessage;

GENT_FILE_XFER_DATA——文件内容

typedef struct SPICE_ATTR_PACKED VDAgentFileXferDataMessage{
    uint32_t id;
    uint64_t size;
    uint8_t data[0];
} VDAgentFileXferDataMessage;

文件传输任务对象

typedef struct SpiceFileXferTask {
    uint32_t                       id;
    gboolean                       pending;
    GFile                          *file;
    SpiceMainChannel               *channel;
    GFileInputStream               *file_stream;
    GFileCopyFlags                 flags;
    GCancellable                   *cancellable;
    GFileProgressCallback          progress_callback;
    gpointer                       progress_callback_data;
    GAsyncReadyCallback            callback;
    gpointer                       user_data;
    char                           buffer[FILE_XFER_CHUNK_SIZE];
    uint64_t                       read_bytes;
    uint64_t                       file_size;
    GError                         *error;
} SpiceFileXferTask;

事件线路

拖拽文件的一刹那,gtk捕捉到了这个事件,交给事先注册好的回调函数

static void spice_display_init(SpiceDisplay *display)
{
    GtkWidget *widget = GTK_WIDGET(display);
    SpiceDisplayPrivate *d;
    GtkTargetEntry targets = { "text/uri-list", 0, 0 };
    d = display->priv = SPICE_DISPLAY_GET_PRIVATE(display);
    g_signal_connect(display, "grab-broken-event", G_CALLBACK(grab_broken), NULL);
    g_signal_connect(display, "grab-notify", G_CALLBACK(grab_notify), NULL);
    gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, &targets, 1, GDK_ACTION_COPY);
    g_signal_connect(display, "drag-data-received",
                     G_CALLBACK(drag_data_received_callback), NULL);
    //......
}

从源码看,初始化的时候为drag-data-received消息注册了drag_data_received_callback()这个回调函数,这个函数根据拖动区域把文件名都获取出来传给spice_main_file_copy_async()

spice_main_file_copy_async()此接口是传输的开始,不过基本没干什么事,官方目前还没支持多文件,所以一上来就判断是否为单文件,否则直接返回g_return_if_fail(sources[1] == NULL);,接着判断agent是否处于连接状态,然后把文件传给file_xfer_send_start_msg_async()处理

此接口负责创建SpiceFileXferTask对象,并且设置好task的一些属性,其中id用于多文件传输的鉴别作用,然后调用g_file_read_async()打开文件并且回调file_xfer_read_async_cb()

file_xfer_read_async_cb()干的事情也不多,设置好task文件句柄,判断是否有错误,错误即退出,否则通过g_file_query_info_async()获取文件属性,之后调用file_xfer_info_async_cb()回调

此回调把获取的文件名、大小等属性序列化为字节序列,创建VDAgentFileXferStartMessage实例,通过agent_msg_queue_many()送出去,即发送VD_AGENT_FILE_XFER_START消息,最后通过spice_channel_wakeup()遍历channel

消息线路

接下来就不是线性的连着的了,先看一幅图:

某个时刻main_agent_handle_msg()会捕捉到VD_AGENT_FILE_XFER_STATUS消息,然后调用file_xfer_handle_status()处理

file_xfer_handle_status()有状态判断的switch-case,如果状态为VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA,这说明可以继续传送,则会调用file_xfer_continue_read()

此接口调用g_input_stream_read_async()读取文件,然后回调file_xfer_read_cb(),此接口调用file_xfer_queue()传输数据,然后通过file_xfer_flush_async()异步读取刷新,然后回调file_xfer_data_flushed_cb(),此接口会判断并调用file_xfer_continue_read(),周而复始; 继续读取的过程中如果失败则会创建VDAgentFileXferStatusMessage,然后发送VD_AGENT_FILE_XFER_STATUS=VD_AGENT_FILE_XFER_STATUS_ERROR的消息

最后,完成传输后会调用file_xfer_completed(),此接口会从队列中移除传输任务,以及其它一些清理工作

其它

在分析过程中有个接口的实现比较巧妙,特别拿出来说下: VD_AGENT_FILE_XFER_START/STATUS/DATA等信息的包装都需要借助agent_msg_queue_many(),这个接口实现如下:

/* any context: the message is not flushed immediately,
   you can wakeup() the channel coroutine or send_msg_queue()

   expected arguments, pair of data/data_size to send terminated with NULL:
   agent_msg_queue_many(main, VD_AGENT_...,
                        &foo, sizeof(Foo),
                        data, data_size, NULL);
*/
G_GNUC_NULL_TERMINATED
static void  (SpiceMainChannel *channel, int type, const void *data, ...)
{
    va_list args;
    SpiceMainChannelPrivate *c = channel->priv;
    SpiceMsgOut *out;
    VDAgentMessage msg;
    guint8 *payload;
    gsize paysize, s, mins, size = 0;
    const guint8 *d;

    G_STATIC_ASSERT(VD_AGENT_MAX_DATA_SIZE > sizeof(VDAgentMessage));

    va_start(args, data);
    for (d = data; d != NULL; d = va_arg(args, void*)) {
        size += va_arg(args, gsize);
    }
    va_end(args);

    msg.protocol = VD_AGENT_PROTOCOL;
    msg.type = type;
    msg.opaque = 0;
    msg.size = size;

    paysize = MIN(VD_AGENT_MAX_DATA_SIZE, size + sizeof(VDAgentMessage));
    out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_AGENT_DATA);
    payload = spice_marshaller_reserve_space(out->marshaller, paysize);
    //先把VDAgentMessage装载到payload,同时移动指针
    memcpy(payload, &msg, sizeof(VDAgentMessage));
    payload += sizeof(VDAgentMessage);
    paysize -= sizeof(VDAgentMessage);
    //这种情况一般不会发生,除非size==0?!仅仅发送空的type消息?
    if (paysize == 0) {
        g_queue_push_tail(c->agent_msg_queue, out);
        out = NULL;
    }
    //下面依次把数据装载到payload
    va_start(args, data);
    for (d = data; size > 0; d = va_arg(args, void*)) {
        s = va_arg(args, gsize);
        while (s > 0) {
            if (out == NULL) {
                paysize = MIN(VD_AGENT_MAX_DATA_SIZE, size);
                out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_AGENT_DATA);
                payload = spice_marshaller_reserve_space(out->marshaller, paysize);
            }
            mins = MIN(paysize, s);
            memcpy(payload, d, mins);
            d += mins;
            payload += mins;
            s -= mins;
            size -= mins;
            paysize -= mins;
            if (paysize == 0) {
                g_queue_push_tail(c->agent_msg_queue, out);
                out = NULL;
            }
        }
    }
    va_end(args);
    g_warn_if_fail(out == NULL);
}

注意到注释里写的”expected arguments, pair of data/data_size to send terminated with NULL”了吗,它就是根据数据和大小配对,实现数据解析的,就会明白下面的代码段功能及后面构建paysize/payload巧妙了:

for (d = data; d != NULL; d = va_arg(args, void*)) {
        size += va_arg(args, gsize);
    }

Tips

至此,文件传输动作分析基本告一段落,这里总结下:

  • 代码里充斥着消息及回调,分析之前一定要有个overview,不然很容易绕晕
  • 分析的工具推荐understand这个代码分析软件,因为它是跨平台的,还有,上面的结构图就是这个自动生成的
  • 因为文件传输走的是main channel,要想彻底了解清楚文件拖拽怎么传输的,还需要熟悉spice channel底层实现机理及agent端代码,这个后面找时间写

参考: http://cgit.freedesktop.org/spice/spice-gtk/ http://lists.freedesktop.org/archives/spice-devel/2013-January/011946.html http://lists.freedesktop.org/archives/spice-devel/2012-November/011485.html http://lists.freedesktop.org/archives/spice-devel/2012-November/011400.html