/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (C) 2009-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 .
*/
/* Replay a previously recorded file (via SPICE_WORKER_RECORD_FILENAME)
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "red-replay-qxl.h"
#include "test_display_base.h"
#include
static SpiceCoreInterface *core;
static SpiceServer *server;
static SpiceReplay *replay;
static QXLWorker *qxl_worker = NULL;
static gboolean started = FALSE;
static QXLInstance display_sin = { 0, };
static gint slow = 0;
static gint skip = 0;
static gboolean print_count = FALSE;
static guint ncommands = 0;
static pid_t client_pid;
static GMainLoop *loop = NULL;
static GAsyncQueue *display_queue = NULL;
static GAsyncQueue *cursor_queue = NULL;
static long total_size;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static GSource *fill_source = NULL;
#define MEM_SLOT_GROUP_ID 0
/* Parts cribbed from spice-display.h/.c/qxl.c */
static QXLDevMemSlot slot = {
.slot_group_id = MEM_SLOT_GROUP_ID,
.slot_id = 0,
.generation = 0,
.virt_start = 0,
.virt_end = ~0,
.addr_delta = 0,
.qxl_ram_size = ~0,
};
static void attach_worker(QXLInstance *qin, QXLWorker *_qxl_worker)
{
static int count = 0;
if (++count > 1) {
g_warning("%s ignored\n", __func__);
return;
}
g_debug("%s\n", __func__);
qxl_worker = _qxl_worker;
spice_qxl_add_memslot(qin, &slot);
spice_server_vm_start(server);
}
static void set_compression_level(QXLInstance *qin, int level)
{
g_debug("%s\n", __func__);
}
static void set_mm_time(QXLInstance *qin, uint32_t mm_time)
{
}
// same as qemu/ui/spice-display.h
#define MAX_SURFACE_NUM 1024
static void get_init_info(QXLInstance *qin, QXLDevInitInfo *info)
{
bzero(info, sizeof(*info));
info->num_memslots = 1;
info->num_memslots_groups = 1;
info->memslot_id_bits = 1;
info->memslot_gen_bits = 1;
info->n_surfaces = MAX_SURFACE_NUM;
}
static gboolean fill_queue_idle(gpointer user_data)
{
gboolean keep = FALSE;
gboolean wakeup = FALSE;
while ((g_async_queue_length(display_queue) +
g_async_queue_length(cursor_queue)) < 50) {
QXLCommandExt *cmd = spice_replay_next_cmd(replay, qxl_worker);
if (!cmd) {
g_async_queue_push(display_queue, GINT_TO_POINTER(-1));
g_async_queue_push(cursor_queue, GINT_TO_POINTER(-1));
goto end;
}
++ncommands;
if (slow && (ncommands > skip)) {
g_usleep(slow);
}
wakeup = TRUE;
if (cmd->cmd.type == QXL_CMD_CURSOR) {
g_async_queue_push(cursor_queue, cmd);
} else {
g_async_queue_push(display_queue, cmd);
}
}
end:
if (!keep) {
pthread_mutex_lock(&mutex);
if (fill_source) {
g_source_destroy(fill_source);
g_source_unref(fill_source);
fill_source = NULL;
}
pthread_mutex_unlock(&mutex);
}
if (wakeup)
spice_qxl_wakeup(&display_sin);
return keep;
}
static void fill_queue(void)
{
pthread_mutex_lock(&mutex);
if (!started)
goto end;
if (fill_source)
goto end;
fill_source = g_idle_source_new();
g_source_set_callback(fill_source, fill_queue_idle, NULL, NULL);
g_source_attach(fill_source, basic_event_loop_get_context());
end:
pthread_mutex_unlock(&mutex);
}
// called from spice_server thread (i.e. red_worker thread)
static int get_command_from(QXLInstance *qin, QXLCommandExt *ext, GAsyncQueue *queue)
{
QXLCommandExt *cmd;
if (g_async_queue_length(queue) == 0) {
/* could use a gcondition ? */
fill_queue();
return FALSE;
}
cmd = g_async_queue_try_pop(queue);
if (GPOINTER_TO_INT(cmd) == -1) {
g_main_loop_quit(loop);
return FALSE;
}
*ext = *cmd;
return TRUE;
}
static int req_notification(GAsyncQueue *queue)
{
/* check for pending messages */
return g_async_queue_length(queue) == 0;
}
static int get_display_command(QXLInstance *qin, QXLCommandExt *ext)
{
return get_command_from(qin, ext, display_queue);
}
static int req_display_notification(QXLInstance *qin)
{
return req_notification(display_queue);
}
static void end_replay(void)
{
int child_status;
/* FIXME: wait threads and end cleanly */
spice_replay_free(replay);
if (client_pid) {
g_debug("kill %d", client_pid);
kill(client_pid, SIGINT);
waitpid(client_pid, &child_status, 0);
}
exit(0);
}
static void release_resource(QXLInstance *qin, struct QXLReleaseInfoExt release_info)
{
spice_replay_free_cmd(replay, (QXLCommandExt *)release_info.info->id);
}
static int get_cursor_command(QXLInstance *qin, struct QXLCommandExt *ext)
{
return get_command_from(qin, ext, cursor_queue);
}
static int req_cursor_notification(QXLInstance *qin)
{
return req_notification(cursor_queue);
}
static void notify_update(QXLInstance *qin, uint32_t update_id)
{
}
static int flush_resources(QXLInstance *qin)
{
return TRUE;
}
static QXLInterface display_sif = {
.base = {
.type = SPICE_INTERFACE_QXL,
.description = "replay",
.major_version = SPICE_INTERFACE_QXL_MAJOR,
.minor_version = SPICE_INTERFACE_QXL_MINOR
},
.attache_worker = attach_worker,
.set_compression_level = set_compression_level,
.set_mm_time = set_mm_time,
.get_init_info = get_init_info,
.get_command = get_display_command,
.req_cmd_notification = req_display_notification,
.release_resource = release_resource,
.get_cursor_command = get_cursor_command,
.req_cursor_notification = req_cursor_notification,
.notify_update = notify_update,
.flush_resources = flush_resources,
};
static void replay_channel_event(int event, SpiceChannelEventInfo *info)
{
if (info->type == SPICE_CHANNEL_DISPLAY &&
event == SPICE_CHANNEL_EVENT_INITIALIZED) {
started = TRUE;
}
}
static gboolean start_client(gchar *cmd, GError **error)
{
gboolean retval;
gint argc;
gchar **argv = NULL;
if (!g_shell_parse_argv(cmd, &argc, &argv, error))
return FALSE;
retval = g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH,
NULL, NULL, &client_pid, error);
g_strfreev(argv);
return retval;
}
static gboolean progress_timer(gpointer user_data)
{
FILE *fd = user_data;
/* it seems somehow thread safe, move to worker thread? */
double pos = (double)ftell(fd);
g_debug("%.2f%%", pos/total_size * 100);
return TRUE;
}
int main(int argc, char **argv)
{
GError *error = NULL;
GOptionContext *context = NULL;
gchar *client = NULL, *codecs = NULL, **file = NULL;
gint port = 5000, compression = SPICE_IMAGE_COMPRESSION_AUTO_GLZ;
gint streaming = SPICE_STREAM_VIDEO_FILTER;
gboolean wait = FALSE;
FILE *fd;
GOptionEntry entries[] = {
{ "client", 'c', 0, G_OPTION_ARG_STRING, &client, "Client", "CMD" },
{ "compression", 'C', 0, G_OPTION_ARG_INT, &compression, "Compression (default 2)", "INT" },
{ "streaming", 'S', 0, G_OPTION_ARG_INT, &streaming, "Streaming (default 3)", "INT" },
{ "video-codecs", 'v', 0, G_OPTION_ARG_STRING, &codecs, "Video codecs", "STRING" },
{ "port", 'p', 0, G_OPTION_ARG_INT, &port, "Server port (default 5000)", "PORT" },
{ "wait", 'w', 0, G_OPTION_ARG_NONE, &wait, "Wait for client", NULL },
{ "slow", 's', 0, G_OPTION_ARG_INT, &slow, "Slow down replay. Delays USEC microseconds before each command", "USEC" },
{ "skip", 0, 0, G_OPTION_ARG_INT, &skip, "skip 'slow' for the first n commands", NULL },
{ "count", 0, 0, G_OPTION_ARG_NONE, &print_count, "Print the number of commands processed", NULL },
{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &file, "replay file", "FILE" },
{ NULL }
};
static const char description[] =
"Compression values:\n"
"\t1=off 2=auto_glz 3=auto_lz 4=quic 5=glz 6=lz 7=lz4\n"
"\n"
"Streaming values:\n"
"\t1=off 2=all 3=filter";
/* these asserts are here to check that the documentation we state above is still correct */
G_STATIC_ASSERT(SPICE_STREAM_VIDEO_OFF == 1);
G_STATIC_ASSERT(SPICE_STREAM_VIDEO_ALL == 2);
G_STATIC_ASSERT(SPICE_STREAM_VIDEO_FILTER == 3);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_INVALID == 0);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_OFF == 1);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_AUTO_GLZ == 2);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_AUTO_LZ == 3);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_QUIC == 4);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_GLZ == 5);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_LZ == 6);
G_STATIC_ASSERT(SPICE_IMAGE_COMPRESSION_LZ4 == 7);
context = g_option_context_new("- replay spice server recording");
g_option_context_add_main_entries(context, entries, NULL);
g_option_context_set_description(context, description);
if (!g_option_context_parse(context, &argc, &argv, &error)) {
g_printerr("Option parsing failed: %s\n", error->message);
exit(1);
}
if (!file) {
g_printerr("%s\n", g_option_context_get_help(context, TRUE, NULL));
exit(1);
}
g_option_context_free(context);
context = NULL;
if (compression <= SPICE_IMAGE_COMPRESSION_INVALID
|| compression >= SPICE_IMAGE_COMPRESSION_ENUM_END) {
g_printerr("invalid compression value\n");
exit(1);
}
if (streaming < 0 || streaming == SPICE_STREAM_VIDEO_INVALID) {
g_printerr("invalid streaming value\n");
exit(1);
}
if (strncmp(file[0], "-", 1) == 0) {
fd = stdin;
} else {
fd = fopen(file[0], "r");
}
if (fd == NULL) {
g_printerr("error opening %s\n", file[0]);
exit(1);
}
g_strfreev(file);
file = NULL;
if (fcntl(fileno(fd), FD_CLOEXEC) < 0) {
perror("fcntl failed");
exit(1);
}
fseek(fd, 0L, SEEK_END);
total_size = ftell(fd);
fseek(fd, 0L, SEEK_SET);
if (total_size > 0)
g_timeout_add_seconds(1, progress_timer, fd);
replay = spice_replay_new(fd, MAX_SURFACE_NUM);
if (replay == NULL) {
g_printerr("Error initializing replay\n");
exit(1);
}
display_queue = g_async_queue_new();
cursor_queue = g_async_queue_new();
core = basic_event_loop_init();
core->channel_event = replay_channel_event;
server = spice_server_new();
spice_server_set_image_compression(server, compression);
spice_server_set_streaming_video(server, streaming);
if (codecs != NULL) {
if (spice_server_set_video_codecs(server, codecs) != 0) {
g_warning("could not set codecs: %s", codecs);
}
g_free(codecs);
}
spice_server_set_port(server, port);
spice_server_set_noauth(server);
g_print("listening on port %d (insecure)\n", port);
spice_server_init(server, core);
display_sin.base.sif = &display_sif.base;
spice_server_add_interface(server, &display_sin.base);
if (client) {
start_client(client, &error);
wait = TRUE;
g_free(client);
client = NULL;
}
if (!wait) {
started = TRUE;
fill_queue();
}
loop = g_main_loop_new(basic_event_loop_get_context(), FALSE);
g_main_loop_run(loop);
if (print_count)
g_print("Counted %d commands\n", ncommands);
end_replay();
g_async_queue_unref(display_queue);
g_async_queue_unref(cursor_queue);
/* FIXME: there should be a way to join server threads before:
* g_main_loop_unref(loop);
*/
return 0;
}