mirror of
https://gitlab.uni-freiburg.de/opensourcevdi/spice
synced 2025-12-27 15:45:54 +00:00
Send wakeups only when a command is available. This emulate better what a guest driver should do (append the command to the ring and then signal). Signed-off-by: Frediano Ziglio <fziglio@redhat.com> Acked-by: Pavel Grunt <pgrunt@redhat.com>
420 lines
11 KiB
C
420 lines
11 KiB
C
/* -*- 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/* Replay a previously recorded file (via SPICE_WORKER_RECORD_FILENAME)
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <sys/types.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <pthread.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <glib.h>
|
|
#include <pthread.h>
|
|
|
|
#include <spice/macros.h>
|
|
#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 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(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);
|
|
|
|
wakeup = TRUE;
|
|
g_async_queue_push(aqueue, 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(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",
|
|
g_source_get_id(fill_source), 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;
|
|
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" },
|
|
{ "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" },
|
|
{ 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");
|
|
}
|
|
g_strfreev(file);
|
|
file = NULL;
|
|
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);
|
|
if (replay == NULL) {
|
|
g_printerr("Error initializing replay\n");
|
|
exit(1);
|
|
}
|
|
|
|
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_streaming_video(server, streaming);
|
|
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);
|
|
|
|
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;
|
|
}
|