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