/* -*- 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 "common/log.h"
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 pid_t client_pid;
static GMainLoop *loop = NULL;
static GAsyncQueue *aqueue = NULL;
static long total_size;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static guint fill_source_id = 0;
#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;
while (g_async_queue_length(aqueue) < 50) {
QXLCommandExt *cmd = spice_replay_next_cmd(replay, qxl_worker);
if (!cmd) {
g_async_queue_push(aqueue, GINT_TO_POINTER(-1));
goto end;
}
if (slow)
g_usleep(slow);
g_async_queue_push(aqueue, cmd);
}
end:
if (!keep) {
pthread_mutex_lock(&mutex);
fill_source_id = 0;
pthread_mutex_unlock(&mutex);
}
spice_qxl_wakeup(&display_sin);
return keep;
}
static void fill_queue(void)
{
pthread_mutex_lock(&mutex);
if (!started)
goto end;
if (fill_source_id != 0)
goto end;
fill_source_id = g_idle_add(fill_queue_idle, NULL);
end:
pthread_mutex_unlock(&mutex);
}
// called from spice_server thread (i.e. red_worker thread)
static int get_command(QXLInstance *qin, QXLCommandExt *ext)
{
QXLCommandExt *cmd;
if (g_async_queue_length(aqueue) == 0) {
/* could use a gcondition ? */
fill_queue();
return FALSE;
}
cmd = g_async_queue_try_pop(aqueue);
if (GPOINTER_TO_INT(cmd) == -1) {
g_main_loop_quit(loop);
return FALSE;
}
*ext = *cmd;
return TRUE;
}
static int req_cmd_notification(QXLInstance *qin)
{
if (!started)
return TRUE;
g_printerr("id: %d, queue length: %d",
fill_source_id, g_async_queue_length(aqueue));
return TRUE;
}
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 FALSE;
}
static int req_cursor_notification(QXLInstance *qin)
{
return TRUE;
}
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_command,
.req_cmd_notification = req_cmd_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, **file = NULL;
gint port = 5000, compression = SPICE_IMAGE_COMPRESSION_AUTO_GLZ;
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" },
{ "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", NULL },
{ G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &file, "replay file", "FILE" },
{ NULL }
};
context = g_option_context_new("- replay spice server recording");
g_option_context_add_main_entries(context, entries, NULL);
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);
}
if (strncmp(file[0], "-", 1) == 0) {
fd = stdin;
} else {
fd = fopen(file[0], "r");
}
if (fd == NULL) {
g_printerr("error opening %s\n", argv[1]);
return 1;
}
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);
aqueue = 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_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;
}
if (!wait) {
started = TRUE;
fill_queue();
}
loop = g_main_loop_new(NULL, FALSE);
g_main_loop_run(loop);
end_replay();
g_async_queue_unref(aqueue);
/* FIXME: there should be a way to join server threads before:
* g_main_loop_unref(loop);
*/
return 0;
}