> > Use the glib mainloop instead of writing our own. The glib loop is both > cleaner to use and is more extensible. It is also very mature and > reduces the maintenance burden on the spice server. > > Signed-off-by: Marc-André Lureau <marcandre.lureau@xxxxxxxxx> > Signed-off-by: Jonathon Jongsma <jjongsma@xxxxxxxxxx> > Signed-off-by: Frediano Ziglio <fziglio@xxxxxxxxxx> > --- > server/Makefile.am | 2 - > server/red-worker.c | 252 +++++++++++++++++------------------------- > server/red-worker.h | 1 + > server/spice_timer_queue.c | 267 > --------------------------------------------- > server/spice_timer_queue.h | 44 -------- > 5 files changed, 99 insertions(+), 467 deletions(-) > delete mode 100644 server/spice_timer_queue.c > delete mode 100644 server/spice_timer_queue.h > > diff --git a/server/Makefile.am b/server/Makefile.am > index bef999d..5d65526 100644 > --- a/server/Makefile.am > +++ b/server/Makefile.am > @@ -120,8 +120,6 @@ libserver_la_SOURCES = \ > sound.h \ > stat.h \ > spicevmc.c \ > - spice_timer_queue.c \ > - spice_timer_queue.h \ > zlib-encoder.c \ > zlib-encoder.h \ > image-cache.h \ > diff --git a/server/red-worker.c b/server/red-worker.c > index ad8ba1a..24bb435 100644 > --- a/server/red-worker.c > +++ b/server/red-worker.c > @@ -49,30 +49,22 @@ > > #include "spice.h" > #include "red-worker.h" > -#include "spice_timer_queue.h" > #include "cursor-channel.h" > #include "tree.h" > > #define CMD_RING_POLL_TIMEOUT 10 //milli > #define CMD_RING_POLL_RETRIES 200 > > -#define MAX_EVENT_SOURCES 20 > #define INF_EVENT_WAIT ~0 > > -struct SpiceWatch { > - struct RedWorker *worker; > - SpiceWatchFunc watch_func; > - void *watch_func_opaque; > -}; > - > struct RedWorker { > pthread_t thread; > QXLInstance *qxl; > RedDispatcher *red_dispatcher; > int running; > SpiceCoreInterfaceInternal core; > - struct pollfd poll_fds[MAX_EVENT_SOURCES]; > - struct SpiceWatch watches[MAX_EVENT_SOURCES]; > + > + GMainContext *main_context; > unsigned int event_timeout; > > DisplayChannel *display_channel; > @@ -99,6 +91,13 @@ struct RedWorker { > FILE *record_fd; > }; > > +GMainContext* red_worker_get_main_context(RedWorker *worker) > +{ > + spice_return_val_if_fail(worker, NULL); > + > + return worker->main_context; > +} > + This function is not used in all code. Also is not used in all refactory branch. Should be just removed. > QXLInstance* red_worker_get_qxl(RedWorker *worker) > { > spice_return_val_if_fail(worker != NULL, NULL); > @@ -510,83 +509,18 @@ static int > common_channel_config_socket(RedChannelClient *rcc) > return TRUE; > } > > -static void worker_watch_update_mask(SpiceWatch *watch, int event_mask) > -{ > - struct RedWorker *worker; > - int i; > - > - if (!watch) { > - return; > - } > - > - worker = watch->worker; > - i = watch - worker->watches; > - > - worker->poll_fds[i].events = 0; > - if (event_mask & SPICE_WATCH_EVENT_READ) { > - worker->poll_fds[i].events |= POLLIN; > - } > - if (event_mask & SPICE_WATCH_EVENT_WRITE) { > - worker->poll_fds[i].events |= POLLOUT; > - } > -} > - > -static SpiceWatch *worker_watch_add(const SpiceCoreInterfaceInternal *iface, > - int fd, int event_mask, SpiceWatchFunc > func, void *opaque) > +static inline GMainContext *event_loop_context_from_iface(const > SpiceCoreInterfaceInternal *iface) > { > RedWorker *worker = SPICE_CONTAINEROF(iface, RedWorker, core); > - int i; > > - /* Search for a free slot in our poll_fds & watches arrays */ > - for (i = 0; i < MAX_EVENT_SOURCES; i++) { > - if (worker->poll_fds[i].fd == -1) { > - break; > - } > - } > - if (i == MAX_EVENT_SOURCES) { > - /* Since we are a channel core implementation, we always get called > from > - red_channel_client_create(), so opaque always is our rcc */ > - RedChannelClient *rcc = opaque; > - spice_warning("could not add a watch for channel type %u id %u", > - rcc->channel->type, rcc->channel->id); > - return NULL; > - } > - > - worker->poll_fds[i].fd = fd; > - worker->watches[i].worker = worker; > - worker->watches[i].watch_func = func; > - worker->watches[i].watch_func_opaque = opaque; > - worker_watch_update_mask(&worker->watches[i], event_mask); > - > - return &worker->watches[i]; > + return worker->main_context; > } > > -static void worker_watch_remove(SpiceWatch *watch) > +static void event_loop_channel_event(int event, SpiceChannelEventInfo *info) > { > - if (!watch) { > - return; > - } > - > - /* Note we don't touch the poll_fd here, to avoid the > - poll_fds/watches table entry getting re-used in the same > - red_worker_main loop over the fds as it is removed. > - > - This is done because re-using it while events were pending on > - the fd previously occupying the slot would lead to incorrectly > - calling the watch_func for the new fd. */ > - memset(watch, 0, sizeof(SpiceWatch)); > } > > -static const SpiceCoreInterfaceInternal worker_core_initializer = { > - .timer_add = spice_timer_queue_add, > - .timer_start = spice_timer_set, > - .timer_cancel = spice_timer_cancel, > - .timer_remove = spice_timer_remove, > - > - .watch_update_mask = worker_watch_update_mask, > - .watch_add = worker_watch_add, > - .watch_remove = worker_watch_remove, > -}; > +#include "event-loop.tmpl.c" > > CommonChannelClient *common_channel_new_client(CommonChannel *common, > int size, > @@ -1524,25 +1458,91 @@ static void register_callbacks(Dispatcher > *dispatcher) > > > > -static void handle_dev_input(int fd, int event, void *opaque) > +static gboolean worker_dispatcher_cb(GIOChannel *source, GIOCondition > condition, > + gpointer data) > { > - RedWorker *worker = opaque; > + RedWorker *worker = data; > > dispatcher_handle_recv_read(red_dispatcher_get_dispatcher(worker->red_dispatcher)); > + > + return TRUE; > } > > +typedef struct RedWorkerSource { > + GSource source; > + RedWorker *worker; > +} RedWorkerSource; > + > +static gboolean worker_source_prepare(GSource *source, gint *p_timeout) > +{ > + RedWorkerSource *wsource = SPICE_CONTAINEROF(source, RedWorkerSource, > source); > + RedWorker *worker = wsource->worker; > + unsigned int timeout; > + > + timeout = MIN(worker->event_timeout, > + > display_channel_get_streams_timeout(worker->display_channel)); > + > + *p_timeout = (timeout == INF_EVENT_WAIT) ? -1 : timeout; > + if (*p_timeout == 0) > + return TRUE; > + > + return FALSE; > +} > + > +static gboolean worker_source_check(GSource *source) > +{ > + RedWorkerSource *wsource = SPICE_CONTAINEROF(source, RedWorkerSource, > source); > + RedWorker *worker = wsource->worker; > + > + return worker->running /* TODO && worker->pending_process */; > +} > + > +static gboolean worker_source_dispatch(GSource *source, GSourceFunc > callback, > + gpointer user_data) > +{ > + RedWorkerSource *wsource = SPICE_CONTAINEROF(source, RedWorkerSource, > source); > + RedWorker *worker = wsource->worker; > + DisplayChannel *display = worker->display_channel; > + int ring_is_empty; > + > + /* during migration, in the dest, the display channel can be initialized > + while the global lz data not since migrate data msg hasn't been > + received yet */ > + /* FIXME: why is this here, and not in display_channel_create */ Good question, still is a TODO > + display_channel_free_glz_drawables_to_free(display); > + > + /* FIXME: could use its own source */ This is more a TODO then a FIXME > + stream_timeout(display); > + > + worker->event_timeout = INF_EVENT_WAIT; > + red_process_cursor(worker, MAX_PIPE_SIZE, &ring_is_empty); > + red_process_display(worker, MAX_PIPE_SIZE, &ring_is_empty); > + > + /* FIXME: remove me? that should be handled by watch out condition */ Good idea too. I think is a TODO even this. Is a good idea because would probably decrease the latency introduced with this patch. About latency, should be useful to use Jonathon patches to check how the loop performs. > + red_push(worker); > + > + return TRUE; > +} > + > +/* cannot be const */ > +static GSourceFuncs worker_source_funcs = { > + .prepare = worker_source_prepare, > + .check = worker_source_check, > + .dispatch = worker_source_dispatch, > +}; > + > RedWorker* red_worker_new(QXLInstance *qxl, RedDispatcher *red_dispatcher) > { > QXLDevInitInfo init_info; > RedWorker *worker; > Dispatcher *dispatcher; > - int i; > const char *record_filename; > > qxl->st->qif->get_init_info(qxl, &init_info); > > worker = spice_new0(RedWorker, 1); > - worker->core = worker_core_initializer; > + worker->core = event_loop_core; > + worker->main_context = g_main_context_new(); > > record_filename = getenv("SPICE_WORKER_RECORD_FILENAME"); > if (record_filename) { > @@ -1576,15 +1576,17 @@ RedWorker* red_worker_new(QXLInstance *qxl, > RedDispatcher *red_dispatcher) > worker->wakeup_counter = stat_add_counter(worker->stat, "wakeups", > TRUE); > worker->command_counter = stat_add_counter(worker->stat, "commands", > TRUE); > #endif > - for (i = 0; i < MAX_EVENT_SOURCES; i++) { > - worker->poll_fds[i].fd = -1; > - } > > - worker->poll_fds[0].fd = dispatcher_get_recv_fd(dispatcher); > - worker->poll_fds[0].events = POLLIN; > - worker->watches[0].worker = worker; > - worker->watches[0].watch_func = handle_dev_input; > - worker->watches[0].watch_func_opaque = worker; > + GIOChannel *channel = > g_io_channel_unix_new(dispatcher_get_recv_fd(dispatcher)); > + GSource *source = g_io_create_watch(channel, G_IO_IN); > + g_source_set_callback(source, (GSourceFunc)worker_dispatcher_cb, worker, > NULL); > + g_source_attach(source, worker->main_context); > + g_source_unref(source); > + Why not using just a watch using our SpiceCoreInterface? > + source = g_source_new(&worker_source_funcs, sizeof(RedWorkerSource)); > + SPICE_CONTAINEROF(source, RedWorkerSource, source)->worker = worker; > + g_source_attach(source, worker->main_context); > + g_source_unref(source); > > memslot_info_init(&worker->mem_slots, > init_info.num_memslots_groups, > @@ -1613,73 +1615,15 @@ SPICE_GNUC_NORETURN static void *red_worker_main(void > *arg) > spice_assert(MAX_PIPE_SIZE > WIDE_CLIENT_ACK_WINDOW && > MAX_PIPE_SIZE > NARROW_CLIENT_ACK_WINDOW); //ensure wakeup by ack > message > > - if (!spice_timer_queue_create()) { > - spice_error("failed to create timer queue"); > - } > - > RED_CHANNEL(worker->cursor_channel)->thread_id = pthread_self(); > RED_CHANNEL(worker->display_channel)->thread_id = pthread_self(); > > - for (;;) { > - int i, num_events; > - unsigned int timeout; > - > - timeout = spice_timer_queue_get_timeout_ms(); > - worker->event_timeout = MIN(timeout, worker->event_timeout); > - timeout = > display_channel_get_streams_timeout(worker->display_channel); > - worker->event_timeout = MIN(timeout, worker->event_timeout); > - num_events = poll(worker->poll_fds, MAX_EVENT_SOURCES, > worker->event_timeout); > - stream_timeout(worker->display_channel); > - spice_timer_queue_cb(); > - > - if (worker->display_channel) { > - /* during migration, in the dest, the display channel can be > initialized > - while the global lz data not since migrate data msg hasn't > been > - received yet */ > - > display_channel_free_glz_drawables_to_free(worker->display_channel); > - } > - > - worker->event_timeout = INF_EVENT_WAIT; > - if (num_events == -1) { > - if (errno != EINTR) { > - spice_error("poll failed, %s", strerror(errno)); > - } > - } > - > - for (i = 0; i < MAX_EVENT_SOURCES; i++) { > - /* The watch may have been removed by the watch-func from > - another fd (ie a disconnect through the dispatcher), > - in this case watch_func is NULL. */ > - if (worker->poll_fds[i].revents && > worker->watches[i].watch_func) { > - int events = 0; > - if (worker->poll_fds[i].revents & POLLIN) { > - events |= SPICE_WATCH_EVENT_READ; > - } > - if (worker->poll_fds[i].revents & POLLOUT) { > - events |= SPICE_WATCH_EVENT_WRITE; > - } > - worker->watches[i].watch_func(worker->poll_fds[i].fd, > events, > - > worker->watches[i].watch_func_opaque); > - } > - } > - > - /* Clear the poll_fd for any removed watches, see the comment in > - watch_remove for why we don't do this there. */ > - for (i = 0; i < MAX_EVENT_SOURCES; i++) { > - if (!worker->watches[i].watch_func) { > - worker->poll_fds[i].fd = -1; > - } > - } > - > - if (worker->running) { > - int ring_is_empty; > - red_process_cursor(worker, MAX_PIPE_SIZE, &ring_is_empty); > - red_process_display(worker, MAX_PIPE_SIZE, &ring_is_empty); > - } > - red_push(worker); > - } > + GMainLoop *loop = g_main_loop_new(worker->main_context, FALSE); > + g_main_loop_run(loop); > + g_main_loop_unref(loop); > > - spice_warn_if_reached(); > + /* FIXME: free worker, and join threads */ > + exit(0); > } > > bool red_worker_run(RedWorker *worker) > diff --git a/server/red-worker.h b/server/red-worker.h > index 91533e1..6bcab3d 100644 > --- a/server/red-worker.h > +++ b/server/red-worker.h > @@ -97,6 +97,7 @@ bool red_worker_run(RedWorker *worker); > QXLInstance* red_worker_get_qxl(RedWorker *worker); > RedChannel* red_worker_get_cursor_channel(RedWorker *worker); > RedChannel* red_worker_get_display_channel(RedWorker *worker); > +GMainContext* red_worker_get_main_context(RedWorker *worker); > RedMemSlotInfo* red_worker_get_memslot(RedWorker *worker); > > void red_drawable_unref(RedWorker *worker, RedDrawable *red_drawable, > diff --git a/server/spice_timer_queue.c b/server/spice_timer_queue.c > deleted file mode 100644 > index 421b090..0000000 > --- a/server/spice_timer_queue.c > +++ /dev/null > @@ -1,267 +0,0 @@ > -/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ > -/* > - Copyright (C) 2013 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/>. > -*/ > -#include <config.h> > -#include <pthread.h> > -#include "red-common.h" > -#include "spice_timer_queue.h" > -#include "common/ring.h" > -#include "utils.h" > - > -static Ring timer_queue_list; > -static int queue_count = 0; > -static pthread_mutex_t queue_list_lock = PTHREAD_MUTEX_INITIALIZER; > - > -static void spice_timer_queue_init(void) > -{ > - ring_init(&timer_queue_list); > -} > - > -struct SpiceTimer { > - RingItem link; > - RingItem active_link; > - > - SpiceTimerFunc func; > - void *opaque; > - > - SpiceTimerQueue *queue; > - > - int is_active; > - uint32_t ms; > - uint64_t expiry_time; > -}; > - > -struct SpiceTimerQueue { > - RingItem link; > - pthread_t thread; > - Ring timers; > - Ring active_timers; > -}; > - > -static SpiceTimerQueue *spice_timer_queue_find(void) > -{ > - pthread_t self = pthread_self(); > - RingItem *queue_item; > - > - RING_FOREACH(queue_item, &timer_queue_list) { > - SpiceTimerQueue *queue = SPICE_CONTAINEROF(queue_item, > SpiceTimerQueue, link); > - > - if (pthread_equal(self, queue->thread) != 0) { > - return queue; > - } > - } > - > - return NULL; > -} > - > -static SpiceTimerQueue *spice_timer_queue_find_with_lock(void) > -{ > - SpiceTimerQueue *queue; > - > - pthread_mutex_lock(&queue_list_lock); > - queue = spice_timer_queue_find(); > - pthread_mutex_unlock(&queue_list_lock); > - return queue; > -} > - > -int spice_timer_queue_create(void) > -{ > - SpiceTimerQueue *queue; > - > - pthread_mutex_lock(&queue_list_lock); > - if (queue_count == 0) { > - spice_timer_queue_init(); > - } > - > - if (spice_timer_queue_find() != NULL) { > - spice_printerr("timer queue was already created for the thread"); > - return FALSE; > - } > - > - queue = spice_new0(SpiceTimerQueue, 1); > - queue->thread = pthread_self(); > - ring_init(&queue->timers); > - ring_init(&queue->active_timers); > - > - ring_add(&timer_queue_list, &queue->link); > - queue_count++; > - > - pthread_mutex_unlock(&queue_list_lock); > - > - return TRUE; > -} > - > -void spice_timer_queue_destroy(void) > -{ > - RingItem *item; > - SpiceTimerQueue *queue; > - > - pthread_mutex_lock(&queue_list_lock); > - queue = spice_timer_queue_find(); > - > - spice_assert(queue != NULL); > - > - while ((item = ring_get_head(&queue->timers))) { > - SpiceTimer *timer; > - > - timer = SPICE_CONTAINEROF(item, SpiceTimer, link); > - spice_timer_remove(timer); > - } > - > - ring_remove(&queue->link); > - free(queue); > - queue_count--; > - > - pthread_mutex_unlock(&queue_list_lock); > -} > - > -SpiceTimer *spice_timer_queue_add(const SpiceCoreInterfaceInternal *iface, > - SpiceTimerFunc func, void *opaque) > -{ > - SpiceTimer *timer = spice_new0(SpiceTimer, 1); > - SpiceTimerQueue *queue = spice_timer_queue_find_with_lock(); > - > - spice_assert(queue != NULL); > - > - ring_item_init(&timer->link); > - ring_item_init(&timer->active_link); > - > - timer->opaque = opaque; > - timer->func = func; > - timer->queue = queue; > - > - ring_add(&queue->timers, &timer->link); > - > - return timer; > -} > - > -static void _spice_timer_set(SpiceTimer *timer, uint32_t ms, uint64_t now) > -{ > - RingItem *next_item; > - SpiceTimerQueue *queue; > - > - if (timer->is_active) { > - spice_timer_cancel(timer); > - } > - > - queue = timer->queue; > - timer->expiry_time = now + ms; > - timer->ms = ms; > - > - RING_FOREACH(next_item, &queue->active_timers) { > - SpiceTimer *next_timer = SPICE_CONTAINEROF(next_item, SpiceTimer, > active_link); > - > - if (timer->expiry_time <= next_timer->expiry_time) { > - break; > - } > - } > - > - if (next_item) { > - ring_add_before(&timer->active_link, next_item); > - } else { > - ring_add_before(&timer->active_link, &queue->active_timers); > - } > - timer->is_active = TRUE; > -} > - > -void spice_timer_set(SpiceTimer *timer, uint32_t ms) > -{ > - spice_assert(pthread_equal(timer->queue->thread, pthread_self()) != 0); > - > - _spice_timer_set(timer, ms, spice_get_monotonic_time_ms()); > -} > - > -void spice_timer_cancel(SpiceTimer *timer) > -{ > - spice_assert(pthread_equal(timer->queue->thread, pthread_self()) != 0); > - > - if (!ring_item_is_linked(&timer->active_link)) { > - spice_assert(!timer->is_active); > - return; > - } > - > - spice_assert(timer->is_active); > - ring_remove(&timer->active_link); > - timer->is_active = FALSE; > -} > - > -void spice_timer_remove(SpiceTimer *timer) > -{ > - spice_assert(timer->queue); > - spice_assert(ring_item_is_linked(&timer->link)); > - spice_assert(pthread_equal(timer->queue->thread, pthread_self()) != 0); > - > - if (timer->is_active) { > - spice_assert(ring_item_is_linked(&timer->active_link)); > - ring_remove(&timer->active_link); > - } > - ring_remove(&timer->link); > - free(timer); > -} > - > -unsigned int spice_timer_queue_get_timeout_ms(void) > -{ > - int64_t now_ms; > - RingItem *head; > - SpiceTimer *head_timer; > - SpiceTimerQueue *queue = spice_timer_queue_find_with_lock(); > - > - spice_assert(queue != NULL); > - > - if (ring_is_empty(&queue->active_timers)) { > - return -1; > - } > - > - head = ring_get_head(&queue->active_timers); > - head_timer = SPICE_CONTAINEROF(head, SpiceTimer, active_link); > - > - now_ms = spice_get_monotonic_time_ms(); > - > - return MAX(0, ((int64_t)head_timer->expiry_time - now_ms)); > -} > - > - > -void spice_timer_queue_cb(void) > -{ > - uint64_t now_ms; > - RingItem *head; > - SpiceTimerQueue *queue = spice_timer_queue_find_with_lock(); > - > - spice_assert(queue != NULL); > - > - if (ring_is_empty(&queue->active_timers)) { > - return; > - } > - > - now_ms = spice_get_monotonic_time_ms(); > - > - while ((head = ring_get_head(&queue->active_timers))) { > - SpiceTimer *timer = SPICE_CONTAINEROF(head, SpiceTimer, > active_link); > - > - if (timer->expiry_time > now_ms) { > - break; > - } else { > - /* Remove active timer before calling the timer function. > - * Timer function could delete the timer making the timer > - * pointer point to freed data. > - */ > - spice_timer_cancel(timer); > - timer->func(timer->opaque); > - /* timer could now be invalid ! */ > - } > - } > -} > diff --git a/server/spice_timer_queue.h b/server/spice_timer_queue.h > deleted file mode 100644 > index b17cecf..0000000 > --- a/server/spice_timer_queue.h > +++ /dev/null > @@ -1,44 +0,0 @@ > -/* > - Copyright (C) 2013 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 _H_SPICE_TIMER_QUEUE > -#define _H_SPICE_TIMER_QUEUE > - > -#include <stdint.h> > -#include "spice.h" > - > -typedef struct SpiceTimerQueue SpiceTimerQueue; > - > -/* create/destroy a timer queue for the current thread. > - * In order to execute the timers functions, spice_timer_queue_cb should be > called > - * periodically, according to spice_timer_queue_get_timeout_ms */ > -int spice_timer_queue_create(void); > -void spice_timer_queue_destroy(void); > - > -SpiceTimer *spice_timer_queue_add(const SpiceCoreInterfaceInternal *iface, > - SpiceTimerFunc func, void *opaque); > -void spice_timer_set(SpiceTimer *timer, uint32_t ms); > -void spice_timer_cancel(SpiceTimer *timer); > -void spice_timer_remove(SpiceTimer *timer); > - > -/* returns the time left till the earliest timer in the queue expires. > - * returns (unsigned)-1 if there are no active timers */ > -unsigned int spice_timer_queue_get_timeout_ms(void); > -/* call the timeout callbacks of all the expired timers */ > -void spice_timer_queue_cb(void); > - > -#endif > -- > 2.4.3 > > _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx http://lists.freedesktop.org/mailman/listinfo/spice-devel