/*
   Copyright (C) 2010 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 first, on Windows will override winsock definitions */
#include <gio/gnetworking.h>
#include <gio/gio.h>
#include <glib.h>
#ifdef G_OS_UNIX
#include <gio/gunixsocketaddress.h>
#endif

#include "spice-client.h"
#include "spice-common.h"
#include "spice-channel-priv.h"
#include "spice-util-priv.h"
#include "spice-session-priv.h"
#include "gio-coroutine.h"
#include "spice-uri-priv.h"
#include "channel-playback-priv.h"
#include "spice-audio-priv.h"

G_STATIC_ASSERT(sizeof(SpiceSessionClass) == sizeof(GObjectClass) + 12 * sizeof(gpointer));

#if !defined(SOL_TCP) && defined(IPPROTO_TCP)
#define SOL_TCP IPPROTO_TCP
#endif
#if !defined(TCP_KEEPIDLE) && defined(TCP_KEEPALIVE) && defined(__APPLE__)
#define TCP_KEEPIDLE TCP_KEEPALIVE
#endif

#define IMAGES_CACHE_SIZE_DEFAULT (1024 * 1024 * 80)
#define MIN_GLZ_WINDOW_SIZE_DEFAULT (1024 * 1024 * 12)
#define MAX_GLZ_WINDOW_SIZE_DEFAULT MIN((LZ_MAX_WINDOW_SIZE * 4), 1024 * 1024 * 64)

struct _SpiceSessionPrivate {
    char              *host;
    char              *unix_path;
    char              *port;
    char              *tls_port;
    char              *username;
    char              *password;
    char              *ca_file;
    char              *ciphers;
    GByteArray        *pubkey;
    GByteArray        *ca;
    char              *cert_subject;
    guint             verify;
    gboolean          read_only;
    SpiceURI          *proxy;
    gchar             *shared_dir;
    gboolean          share_dir_ro;

    /* whether to enable audio */
    gboolean          audio;

    /* whether to enable smartcard event forwarding to the server */
    gboolean          smartcard;

    /* whether to enable GL scanout */
    gboolean          gl_scanout;

    /* list of certificates to use for the software smartcard reader if
     * enabled. For now, it has to contain exactly 3 certificates for
     * the software reader to be functional
     */
    GStrv             smartcard_certificates;

    /* path to the local certificate database to use to lookup the
     * certificates stored in 'certificates'. If NULL, libcacard will
     * fallback to using a default database.
     */
    char *            smartcard_db;

    /* whether to enable USB redirection */
    gboolean          usbredir;

    /* Set when a usbredir channel has requested the keyboard grab to be
       temporarily released (because it is going to invoke policykit) */
    gboolean          inhibit_keyboard_grab;

    GStrv             disable_effects;
    GStrv             secure_channels;

    int               connection_id;
    int               protocol;
    SpiceChannel      *cmain; /* weak reference */
    GList             *channels;
    guint             channels_destroying;
    gboolean          client_provided_sockets;
    guint64           mm_time_offset;
    SpiceSession      *migration;
    GList             *migration_left;
    SpiceSessionMigration migration_state;
    gboolean          full_migration; /* seamless migration indicator */
    guint             disconnecting;
    gboolean          migrate_wait_init;
    guint             after_main_init;
    gboolean          for_migration;

    display_cache     *images;
    SpiceGlzDecoderWindow *glz_window;
    int               images_cache_size;
    int               glz_window_size;
    uint32_t          n_display_channels;
    guint8            uuid[16];
    gchar             *name;
    SpiceImageCompression preferred_compression;

    /* associated objects */
    SpiceAudio        *audio_manager;
    SpiceUsbDeviceManager *usb_manager;
    SpicePlaybackChannel *playback_channel;
    PhodavServer      *webdav;
};


/**
 * SECTION:spice-session
 * @short_description: handles connection details, and active channels
 * @title: Spice Session
 * @section_id:
 * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
 * @stability: Stable
 * @include: spice-client.h
 *
 * The #SpiceSession class handles all the #SpiceChannel connections.
 * It's also the class that contains connections informations, such as
 * #SpiceSession:host and #SpiceSession:port.
 *
 * You can simply set the property #SpiceSession:uri to something like
 * "spice://127.0.0.1?port=5930" to configure your connection details.
 *
 * You may want to connect to #SpiceSession::channel-new signal, to be
 * informed of the availability of channels and to interact with
 * them.
 *
 * For example, when the #SpiceInputsChannel is available and get the
 * event #SPICE_CHANNEL_OPENED, you can send key events with
 * spice_inputs_key_press(). When the #SpiceMainChannel is available,
 * you can start sharing the clipboard...  .
 *
 *
 * Once #SpiceSession properties set, you can call
 * spice_session_connect() to start connecting and communicating with
 * a Spice server.
 */

G_DEFINE_TYPE_WITH_PRIVATE(SpiceSession, spice_session, G_TYPE_OBJECT)

/* Properties */
enum {
    PROP_0,
    PROP_HOST,
    PROP_PORT,
    PROP_TLS_PORT,
    PROP_PASSWORD,
    PROP_CA_FILE,
    PROP_CIPHERS,
    PROP_IPV4,
    PROP_IPV6,
    PROP_PROTOCOL,
    PROP_URI,
    PROP_CLIENT_SOCKETS,
    PROP_PUBKEY,
    PROP_CERT_SUBJECT,
    PROP_VERIFY,
    PROP_MIGRATION_STATE,
    PROP_AUDIO,
    PROP_SMARTCARD,
    PROP_SMARTCARD_CERTIFICATES,
    PROP_SMARTCARD_DB,
    PROP_USBREDIR,
    PROP_INHIBIT_KEYBOARD_GRAB,
    PROP_DISABLE_EFFECTS,
    PROP_COLOR_DEPTH,
    PROP_READ_ONLY,
    PROP_CACHE_SIZE,
    PROP_GLZ_WINDOW_SIZE,
    PROP_UUID,
    PROP_NAME,
    PROP_CA,
    PROP_PROXY,
    PROP_SECURE_CHANNELS,
    PROP_SHARED_DIR,
    PROP_SHARE_DIR_RO,
    PROP_WEBDAV_SERVER,
    PROP_USERNAME,
    PROP_UNIX_PATH,
    PROP_PREF_COMPRESSION,
    PROP_GL_SCANOUT,
};

/* signals */
enum {
    SPICE_SESSION_CHANNEL_NEW,
    SPICE_SESSION_CHANNEL_DESTROY,
    SPICE_SESSION_MM_TIME_RESET,
    SPICE_SESSION_DISCONNECTED,
    SPICE_SESSION_LAST_SIGNAL,
};

/* Register SpiceImageCompress */
#define SPICE_TYPE_IMAGE_COMPRESSION spice_image_compress_get_type()
GType spice_image_compress_get_type (void);

static const GEnumValue _spice_image_compress_values[] = {
  { SPICE_IMAGE_COMPRESSION_INVALID, "SPICE_IMAGE_COMPRESSION_INVALID", "invalid" },
  { SPICE_IMAGE_COMPRESSION_OFF, "SPICE_IMAGE_COMPRESSION_OFF", "off" },
  { SPICE_IMAGE_COMPRESSION_AUTO_GLZ, "SPICE_IMAGE_COMPRESSION_AUTO_GLZ", "auto-glz" },
  { SPICE_IMAGE_COMPRESSION_AUTO_LZ, "SPICE_IMAGE_COMPRESSION_AUTO_LZ", "auto-lz" },
  { SPICE_IMAGE_COMPRESSION_QUIC, "SPICE_IMAGE_COMPRESSION_QUIC", "quic" },
  { SPICE_IMAGE_COMPRESSION_GLZ, "SPICE_IMAGE_COMPRESSION_GLZ", "glz" },
  { SPICE_IMAGE_COMPRESSION_LZ, "SPICE_IMAGE_COMPRESSION_LZ", "lz" },
  { SPICE_IMAGE_COMPRESSION_LZ4, "SPICE_IMAGE_COMPRESSION_LZ4", "lz4" },
  { 0, NULL, NULL }
};

G_STATIC_ASSERT(G_N_ELEMENTS(_spice_image_compress_values) == SPICE_IMAGE_COMPRESSION_ENUM_END + 1);

static const gchar* spice_session_get_shared_dir(SpiceSession *session);
static void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir);

GType
spice_image_compress_get_type (void)
{
  static GType type = 0;
  static gsize type_once = 0;

  if (g_once_init_enter(&type_once)) {
    type = g_enum_register_static ("SpiceImageCompress", _spice_image_compress_values);
    g_once_init_leave(&type_once, type);
  }

  return type;
}

static guint signals[SPICE_SESSION_LAST_SIGNAL];

static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel);

static void update_proxy(SpiceSession *self, const gchar *str)
{
    SpiceSessionPrivate *s = self->priv;
    SpiceURI *proxy = NULL;
    GError *error = NULL;

    if (str == NULL)
        str = g_getenv("SPICE_PROXY");
    if (str == NULL || *str == 0) {
        g_clear_object(&s->proxy);
        return;
    }

    proxy = spice_uri_new();
    if (!spice_uri_parse(proxy, str, &error))
        g_clear_object(&proxy);
    if (error) {
        g_warning("%s", error->message);
        g_clear_error(&error);
    }

    if (proxy != NULL) {
        g_clear_object(&s->proxy);
        s->proxy = proxy;
    }
}

static void spice_session_init(SpiceSession *session)
{
    SpiceSessionPrivate *s;
    gchar *channels;

    SPICE_DEBUG("New session (compiled from package " PACKAGE_STRING ")");
    s = session->priv = spice_session_get_instance_private(session);

    channels = spice_channel_supported_string();
    SPICE_DEBUG("Supported channels: %s", channels);
    g_free(channels);

    s->images = cache_image_new((GDestroyNotify)pixman_image_unref);
    s->glz_window = glz_decoder_window_new();
    update_proxy(session, NULL);
}

static void
session_disconnect(SpiceSession *self, gboolean keep_main)
{
    SpiceSessionPrivate *s;

    s = self->priv;

    for (GList *l = s->channels; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        if (keep_main && channel == s->cmain) {
            spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);
        } else {
            spice_session_channel_destroy(self, channel);
        }
    }

    s->connection_id = 0;

    g_clear_pointer(&s->name, g_free);
    memset(s->uuid, 0, sizeof(s->uuid));

    spice_session_abort_migration(self);
}

static void
spice_session_dispose(GObject *gobject)
{
    SpiceSession *session = SPICE_SESSION(gobject);
    SpiceSessionPrivate *s = session->priv;

    SPICE_DEBUG("session dispose");

    session_disconnect(session, FALSE);

    g_warn_if_fail(s->migration == NULL);
    g_warn_if_fail(s->migration_left == NULL);
    g_warn_if_fail(s->after_main_init == 0);
    g_warn_if_fail(s->disconnecting == 0);
    g_warn_if_fail(s->channels_destroying == 0);
    g_warn_if_fail(s->channels == NULL);

    g_clear_object(&s->audio_manager);
    g_clear_object(&s->usb_manager);
    g_clear_object(&s->proxy);
    g_clear_object(&s->webdav);

    /* Chain up to the parent class */
    if (G_OBJECT_CLASS(spice_session_parent_class)->dispose)
        G_OBJECT_CLASS(spice_session_parent_class)->dispose(gobject);
}

static void
spice_session_finalize(GObject *gobject)
{
    SpiceSession *session = SPICE_SESSION(gobject);
    SpiceSessionPrivate *s = session->priv;

    /* release stuff */
    g_free(s->unix_path);
    g_free(s->host);
    g_free(s->port);
    g_free(s->tls_port);
    g_free(s->username);
    g_free(s->password);
    g_free(s->ca_file);
    g_free(s->ciphers);
    g_free(s->cert_subject);
    g_strfreev(s->smartcard_certificates);
    g_free(s->smartcard_db);
    g_strfreev(s->disable_effects);
    g_strfreev(s->secure_channels);
    g_free(s->shared_dir);

    g_clear_pointer(&s->images, cache_free);
    glz_decoder_window_destroy(s->glz_window);

    g_clear_pointer(&s->pubkey, g_byte_array_unref);
    g_clear_pointer(&s->ca, g_byte_array_unref);

    /* Chain up to the parent class */
    if (G_OBJECT_CLASS(spice_session_parent_class)->finalize)
        G_OBJECT_CLASS(spice_session_parent_class)->finalize(gobject);
}

#define URI_SCHEME_SPICE "spice://"
#define URI_SCHEME_SPICE_UNIX "spice+unix://"
#define URI_SCHEME_SPICE_TLS "spice+tls://"
#define URI_QUERY_START ";?"
#define URI_QUERY_SEP   ";&"

static gchar* spice_uri_create(SpiceSession *session)
{
    SpiceSessionPrivate *s = session->priv;

    if (s->unix_path != NULL) {
        return g_strdup_printf(URI_SCHEME_SPICE_UNIX "%s", s->unix_path);
    } else if (s->host != NULL) {
        const char *port, *scheme;
        g_return_val_if_fail(s->port != NULL || s->tls_port != NULL, NULL);

        if (s->tls_port && s->port) {
            /* both set, use spice://foo?port=4390&tls-port= form */
            return g_strdup_printf(URI_SCHEME_SPICE "%s?port=%s&tls-port=%s",
                                   s->host, s->port, s->tls_port);
        }

        /* one set, use spice://foo:4390 or spice+tls://.. form */
        if (s->tls_port) {
            scheme = URI_SCHEME_SPICE_TLS;
            port = s->tls_port;
        } else {
            scheme = URI_SCHEME_SPICE;
            port = s->port;
        }
        return g_strdup_printf("%s%s:%s", scheme, s->host, port);
    }

    g_return_val_if_reached(NULL);
}

static int spice_parse_uri(SpiceSession *session, const char *original_uri)
{
    SpiceSessionPrivate *s = session->priv;
    gchar *host = NULL, *port = NULL, *tls_port = NULL, *uri = NULL, *username = NULL, *password = NULL;
    gchar *path = NULL;
    gchar *authority = NULL;
    gchar *query = NULL;
    gchar *tmp = NULL;
    bool tls_scheme = false;

    g_return_val_if_fail(original_uri != NULL, -1);

    uri = g_strdup(original_uri);

    if (g_str_has_prefix(uri, URI_SCHEME_SPICE_UNIX)) {
        path = uri + strlen(URI_SCHEME_SPICE_UNIX);
        goto end;
    }

    /* Break up the URI into its various parts, scheme, authority,
     * path (ignored) and query
     */
    if (g_str_has_prefix(uri, URI_SCHEME_SPICE)) {
        authority = uri + strlen(URI_SCHEME_SPICE);
    } else if (g_str_has_prefix(uri, URI_SCHEME_SPICE_TLS)) {
        authority = uri + strlen(URI_SCHEME_SPICE_TLS);
        tls_scheme = true;
    } else {
        g_warning("Expected a URI scheme of '%s' in URI '%s'",
                  URI_SCHEME_SPICE, uri);
        goto fail;
    }

    tmp = strchr(authority, '@');
    if (tmp) {
        tmp[0] = '\0';
        username = g_uri_unescape_string(authority, NULL);
        authority = ++tmp;
        tmp = NULL;
    }

    path = strchr(authority, '/');
    if (path) {
        path[0] = '\0';
        path++;
    }

    if (path) {
        size_t prefix = strcspn(path, URI_QUERY_START);
        query = path + prefix;
    } else {
        size_t prefix = strcspn(authority, URI_QUERY_START);
        query = authority + prefix;
    }

    if (query && query[0]) {
        query[0] = '\0';
        query++;
    }

    /* Now process the individual parts */

    if (authority[0] == '[') {
        tmp = strchr(authority, ']');
        if (!tmp) {
            g_warning("Missing closing ']' in authority for URI '%s'", uri);
            goto fail;
        }
        tmp[0] = '\0';
        tmp++;
        host = g_strdup_printf("[%s]", authority + 1);
        if (tmp[0] == ':')
            port = g_strdup(tmp + 1);
    } else {
        tmp = strchr(authority, ':');
        if (tmp) {
            *tmp = '\0';
            tmp++;
            port = g_strdup(tmp);
        }
        host = g_uri_unescape_string(authority, NULL);
    }

    if (path && !(g_str_equal(path, "") ||
                  g_str_equal(path, "/"))) {
        g_warning("Unexpected path data '%s' for URI '%s'", path, uri);
        /* don't fail, just ignore */
    }
    path = NULL;

    while (query && query[0] != '\0') {
        gchar key[32], value[128];
        gchar **target_key;

        int len;
        if (sscanf(query, "%31[-a-zA-Z0-9]=%n", key, &len) != 1) {
            g_warning("Failed to parse key in URI '%s'", query);
            goto fail;
        }

        query += len;
        if (*query == '\0') {
            SPICE_DEBUG("key '%s' without value", key);
            break;
        } else if (*query == ';' || *query == '&') {
            /* another argument */
            query++;
            continue;
        }

        if (sscanf(query, "%127[^;&]%n", value, &len) != 1) {
            g_warning("Failed to parse value of key '%s' in URI '%s'", key, query);
            goto fail;
        }

        query += len;
        if (*query)
            query++;

        if (tls_scheme && (g_str_equal(key, "port") || g_str_equal(key, "tls-port"))) {
            g_warning(URI_SCHEME_SPICE_TLS " scheme doesn't accept '%s'", key);
            continue;
        }

        target_key = NULL;
        if (g_str_equal(key, "port")) {
            target_key = &port;
        } else if (g_str_equal(key, "tls-port")) {
            target_key = &tls_port;
        } else if (g_str_equal(key, "password")) {
            target_key = &password;
            g_warning("password may be visible in process listings");
        } else {
            g_warning("unknown key in spice URI parsing: '%s'", key);
            goto fail;
        }
        if (target_key) {
            if (*target_key) {
                g_warning("Double set of '%s' in URI '%s'", key, original_uri);
                goto fail;
            }
            *target_key = g_uri_unescape_string(value, NULL);
        }
    }

    if (port == NULL && tls_port == NULL) {
        g_warning("Missing port or tls-port in spice URI '%s'", original_uri);
        goto fail;
    }

end:
    /* parsed ok -> apply */
    g_free(s->unix_path);
    g_free(s->host);
    g_free(s->port);
    g_free(s->tls_port);
    g_free(s->username);
    g_free(s->password);
    s->unix_path = g_strdup(path);
    g_free(uri);
    s->host = host;
    if (tls_scheme) {
        s->tls_port = port;
    } else {
        s->port = port;
        s->tls_port = tls_port;
    }
    s->username = username;
    s->password = password;
    return 0;

fail:
    g_free(uri);
    g_free(host);
    g_free(port);
    g_free(tls_port);
    g_free(username);
    g_free(password);
    return -1;
}

static void spice_session_get_property(GObject    *gobject,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
{
    SpiceSession *session = SPICE_SESSION(gobject);
    SpiceSessionPrivate *s = session->priv;

    switch (prop_id) {
    case PROP_HOST:
        g_value_set_string(value, s->host);
	break;
    case PROP_UNIX_PATH:
        g_value_set_string(value, s->unix_path);
        break;
    case PROP_PORT:
        g_value_set_string(value, s->port);
	break;
    case PROP_TLS_PORT:
        g_value_set_string(value, s->tls_port);
	break;
    case PROP_USERNAME:
        g_value_set_string(value, s->username);
	break;
    case PROP_PASSWORD:
        g_value_set_string(value, s->password);
	break;
    case PROP_CA_FILE:
        g_value_set_string(value, s->ca_file);
	break;
    case PROP_CIPHERS:
        g_value_set_string(value, s->ciphers);
	break;
    case PROP_PROTOCOL:
        g_value_set_int(value, s->protocol);
	break;
    case PROP_URI:
        g_value_take_string(value, spice_uri_create(session));
        break;
    case PROP_CLIENT_SOCKETS:
        g_value_set_boolean(value, s->client_provided_sockets);
	break;
    case PROP_PUBKEY:
        g_value_set_boxed(value, s->pubkey);
	break;
    case PROP_CA:
        g_value_set_boxed(value, s->ca);
	break;
    case PROP_CERT_SUBJECT:
        g_value_set_string(value, s->cert_subject);
	break;
    case PROP_VERIFY:
        g_value_set_flags(value, s->verify);
        break;
    case PROP_MIGRATION_STATE:
        g_value_set_enum(value, s->migration_state);
        break;
    case PROP_SMARTCARD:
        g_value_set_boolean(value, s->smartcard);
        break;
    case PROP_SMARTCARD_CERTIFICATES:
        g_value_set_boxed(value, s->smartcard_certificates);
        break;
    case PROP_SMARTCARD_DB:
        g_value_set_string(value, s->smartcard_db);
        break;
    case PROP_USBREDIR:
        g_value_set_boolean(value, s->usbredir);
        break;
    case PROP_INHIBIT_KEYBOARD_GRAB:
        g_value_set_boolean(value, s->inhibit_keyboard_grab);
        break;
    case PROP_DISABLE_EFFECTS:
        g_value_set_boxed(value, s->disable_effects);
        break;
    case PROP_SECURE_CHANNELS:
        g_value_set_boxed(value, s->secure_channels);
        break;
    case PROP_COLOR_DEPTH: /* FIXME: deprecated */
        g_value_set_int(value, 0);
        break;
    case PROP_AUDIO:
        g_value_set_boolean(value, s->audio);
        break;
    case PROP_READ_ONLY:
        g_value_set_boolean(value, s->read_only);
        break;
    case PROP_CACHE_SIZE:
        g_value_set_int(value, s->images_cache_size);
        break;
    case PROP_GLZ_WINDOW_SIZE:
        g_value_set_int(value, s->glz_window_size);
        break;
    case PROP_NAME:
        g_value_set_string(value, s->name);
	break;
    case PROP_UUID:
        g_value_set_pointer(value, s->uuid);
	break;
    case PROP_PROXY:
        g_value_take_string(value, spice_uri_to_string(s->proxy));
	break;
    case PROP_SHARED_DIR:
        g_value_set_string(value, spice_session_get_shared_dir(session));
        break;
    case PROP_SHARE_DIR_RO:
        g_value_set_boolean(value, s->share_dir_ro);
        break;
    case PROP_WEBDAV_SERVER:
        g_value_set_object(value, spice_session_get_webdav_server(session));
        break;
    case PROP_PREF_COMPRESSION:
        g_value_set_enum(value, s->preferred_compression);
        break;
    case PROP_GL_SCANOUT:
        g_value_set_boolean(value, s->gl_scanout);
        break;
    default:
	G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
	break;
    }
}

static void spice_session_set_property(GObject      *gobject,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
    SpiceSession *session = SPICE_SESSION(gobject);
    SpiceSessionPrivate *s = session->priv;
    const char *str;

    switch (prop_id) {
    case PROP_HOST:
        g_free(s->host);
        s->host = g_value_dup_string(value);
        break;
    case PROP_UNIX_PATH:
        g_free(s->unix_path);
        s->unix_path = g_value_dup_string(value);
        break;
    case PROP_PORT:
        g_free(s->port);
        s->port = g_value_dup_string(value);
        break;
    case PROP_TLS_PORT:
        g_free(s->tls_port);
        s->tls_port = g_value_dup_string(value);
        break;
    case PROP_USERNAME:
        g_free(s->username);
        s->username = g_value_dup_string(value);
        break;
    case PROP_PASSWORD:
        g_free(s->password);
        s->password = g_value_dup_string(value);
        break;
    case PROP_CA_FILE:
        g_free(s->ca_file);
        s->ca_file = g_value_dup_string(value);
        break;
    case PROP_CIPHERS:
        g_free(s->ciphers);
        s->ciphers = g_value_dup_string(value);
        break;
    case PROP_PROTOCOL:
        s->protocol = g_value_get_int(value);
        break;
    case PROP_URI:
        str = g_value_get_string(value);
        if (str != NULL)
            spice_parse_uri(session, str);
        break;
    case PROP_CLIENT_SOCKETS:
        s->client_provided_sockets = g_value_get_boolean(value);
        break;
    case PROP_PUBKEY:
        if (s->pubkey)
            g_byte_array_unref(s->pubkey);
        s->pubkey = g_value_dup_boxed(value);
        if (s->pubkey)
            s->verify |= SPICE_SESSION_VERIFY_PUBKEY;
        else
            s->verify &= ~SPICE_SESSION_VERIFY_PUBKEY;
	break;
    case PROP_CERT_SUBJECT:
        g_free(s->cert_subject);
        s->cert_subject = g_value_dup_string(value);
        if (s->cert_subject)
            s->verify |= SPICE_SESSION_VERIFY_SUBJECT;
        else
            s->verify &= ~SPICE_SESSION_VERIFY_SUBJECT;
        break;
    case PROP_VERIFY:
        s->verify = g_value_get_flags(value);
        break;
    case PROP_MIGRATION_STATE:
        s->migration_state = g_value_get_enum(value);
        break;
    case PROP_SMARTCARD:
        s->smartcard = g_value_get_boolean(value);
        break;
    case PROP_SMARTCARD_CERTIFICATES:
        g_strfreev(s->smartcard_certificates);
        s->smartcard_certificates = g_value_dup_boxed(value);
        break;
    case PROP_SMARTCARD_DB:
        g_free(s->smartcard_db);
        s->smartcard_db = g_value_dup_string(value);
        break;
    case PROP_USBREDIR:
        s->usbredir = g_value_get_boolean(value);
        break;
    case PROP_INHIBIT_KEYBOARD_GRAB:
        s->inhibit_keyboard_grab = g_value_get_boolean(value);
        break;
    case PROP_DISABLE_EFFECTS:
        g_strfreev(s->disable_effects);
        s->disable_effects = g_value_dup_boxed(value);
        break;
    case PROP_SECURE_CHANNELS:
        g_strfreev(s->secure_channels);
        s->secure_channels = g_value_dup_boxed(value);
        break;
    case PROP_COLOR_DEPTH:
        spice_info("SpiceSession::color-depth has been deprecated. Property is ignored");
        break;
    case PROP_AUDIO:
        s->audio = g_value_get_boolean(value);
        break;
    case PROP_READ_ONLY:
        s->read_only = g_value_get_boolean(value);
        g_coroutine_object_notify(gobject, "read-only");
        break;
    case PROP_CACHE_SIZE:
        s->images_cache_size = g_value_get_int(value);
        break;
    case PROP_GLZ_WINDOW_SIZE:
        s->glz_window_size = g_value_get_int(value);
        break;
    case PROP_CA:
        g_clear_pointer(&s->ca, g_byte_array_unref);
        s->ca = g_value_dup_boxed(value);
        break;
    case PROP_PROXY:
        update_proxy(session, g_value_get_string(value));
        break;
    case PROP_SHARED_DIR:
        spice_session_set_shared_dir(session, g_value_get_string(value));
        break;
    case PROP_SHARE_DIR_RO:
        s->share_dir_ro = g_value_get_boolean(value);
        break;
    case PROP_PREF_COMPRESSION:
        s->preferred_compression = g_value_get_enum(value);
        break;
    case PROP_GL_SCANOUT:
#ifdef G_OS_UNIX
        s->gl_scanout = g_value_get_boolean(value);
#else
        g_warning("SpiceSession:gl-scanout is only available on Unix");
#endif
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
        break;
    }
}

static void spice_session_class_init(SpiceSessionClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->dispose      = spice_session_dispose;
    gobject_class->finalize     = spice_session_finalize;
    gobject_class->get_property = spice_session_get_property;
    gobject_class->set_property = spice_session_set_property;

    /**
     * SpiceSession:host:
     *
     * URL of the SPICE host to connect to
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_HOST,
         g_param_spec_string("host",
                             "Host",
                             "Remote host",
                             "localhost",
                             G_PARAM_READWRITE |
                             G_PARAM_CONSTRUCT |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:unix-path:
     *
     * Path of the Unix socket to connect to
     *
     * Since: 0.28
     **/
    g_object_class_install_property
        (gobject_class, PROP_UNIX_PATH,
         g_param_spec_string("unix-path",
                             "Unix path",
                             "Unix path",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_CONSTRUCT |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:port:
     *
     * Port to connect to for unencrypted sessions
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_PORT,
         g_param_spec_string("port",
                             "Port",
                             "Remote port (plaintext)",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:tls-port:
     *
     * Port to connect to for TLS sessions
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_TLS_PORT,
         g_param_spec_string("tls-port",
                             "TLS port",
                             "Remote port (encrypted)",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:username:
     *
     * Username to use
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_USERNAME,
         g_param_spec_string("username",
                             "Username",
                             "Username used for SASL connections",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:password:
     *
     * TLS password to use
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_PASSWORD,
         g_param_spec_string("password",
                             "Password",
                             "",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:ca-file:
     *
     * File holding the CA certificates for the host the client is
     * connecting to
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_CA_FILE,
         g_param_spec_string("ca-file",
                             "CA file",
                             "File holding the CA certificates",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:ciphers:
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_CIPHERS,
         g_param_spec_string("ciphers",
                             "Ciphers",
                             "SSL cipher list",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:protocol:
     *
     * Version of the SPICE protocol to use
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_PROTOCOL,
         g_param_spec_int("protocol",
                          "Protocol",
                          "Spice protocol major version",
                          1, 2, 2,
                          G_PARAM_READWRITE |
                          G_PARAM_CONSTRUCT |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:uri:
     *
     * URI of the SPICE host to connect to. The URI is of the form
     * spice://hostname?port=XXX or spice://hostname?tls_port=XXX
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_URI,
         g_param_spec_string("uri",
                             "URI",
                             "Spice connection URI",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:client-sockets:
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_CLIENT_SOCKETS,
         g_param_spec_boolean("client-sockets",
                          "Client sockets",
                          "Sockets are provided by the client",
                          FALSE,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:pubkey:
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_PUBKEY,
         g_param_spec_boxed("pubkey",
                            "Pub Key",
                            "Public key to check",
                            G_TYPE_BYTE_ARRAY,
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:cert-subject:
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_CERT_SUBJECT,
         g_param_spec_string("cert-subject",
                             "Cert Subject",
                             "Certificate subject to check",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:verify:
     *
     * #SpiceSessionVerify bit field indicating which parts of the peer
     * certificate should be checked
     **/
    g_object_class_install_property
        (gobject_class, PROP_VERIFY,
         g_param_spec_flags("verify",
                            "Verify",
                            "Certificate verification parameters",
                            SPICE_TYPE_SESSION_VERIFY,
                            SPICE_SESSION_VERIFY_HOSTNAME,
                            G_PARAM_READWRITE |
                            G_PARAM_CONSTRUCT |
                            G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:migration-state:
     *
     * #SpiceSessionMigration bit field indicating if a migration is in
     * progress
     *
     **/
    g_object_class_install_property
        (gobject_class, PROP_MIGRATION_STATE,
         g_param_spec_enum("migration-state",
                           "Migration state",
                           "Migration state",
                           SPICE_TYPE_SESSION_MIGRATION,
                           SPICE_SESSION_MIGRATION_NONE,
                           G_PARAM_READABLE |
                           G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:disable-effects:
     *
     * A string array of effects to disable. The settings will
     * be applied on new display channels. The following effets can be
     * disabled "wallpaper", "font-smooth", "animation", and "all",
     * which will disable all the effects. If NULL, don't apply changes.
     *
     * Since: 0.7
     **/
    g_object_class_install_property
        (gobject_class, PROP_DISABLE_EFFECTS,
         g_param_spec_boxed ("disable-effects",
                             "Disable effects",
                             "Comma-separated effects to disable",
                             G_TYPE_STRV,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:color-depth:
     *
     * Display color depth to set on new display channels. If 0, don't set.
     *
     * Since: 0.7
     *
     * Deprecated: 0.37: Deprecated due lack of support in drivers, only Windows 7 and older.
     * This option is currently ignored.
     **/
    g_object_class_install_property
        (gobject_class, PROP_COLOR_DEPTH,
         g_param_spec_int("color-depth",
                          "Color depth",
                          "Display channel color depth",
                          0, 32, 0,
                          G_PARAM_DEPRECATED |
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:enable-smartcard:
     *
     * If set to TRUE, the smartcard channel will be enabled and smartcard
     * events will be forwarded to the guest
     *
     * Since: 0.7
     **/
    g_object_class_install_property
        (gobject_class, PROP_SMARTCARD,
         g_param_spec_boolean("enable-smartcard",
                          "Enable smartcard event forwarding",
                          "Forward smartcard events to the SPICE server",
                          FALSE,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:enable-audio:
     *
     * If set to TRUE, the audio channels will be enabled for
     * playback and recording.
     *
     * Since: 0.8
     **/
    g_object_class_install_property
        (gobject_class, PROP_AUDIO,
         g_param_spec_boolean("enable-audio",
                          "Enable audio channels",
                          "Enable audio channels",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:smartcard-certificates:
     *
     * This property is used when one wants to simulate a smartcard with no
     * hardware smartcard reader. If it's set to a NULL-terminated string
     * array containing the names of 3 valid certificates, these will be
     * used to simulate a smartcard in the guest
     * See also spice_smartcard_manager_insert_card()
     *
     * Since: 0.7
     **/
    g_object_class_install_property
        (gobject_class, PROP_SMARTCARD_CERTIFICATES,
         g_param_spec_boxed("smartcard-certificates",
                            "Smartcard certificates",
                            "Smartcard certificates for software-based smartcards",
                            G_TYPE_STRV,
                            G_PARAM_READABLE |
                            G_PARAM_WRITABLE |
                            G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:smartcard-db:
     *
     * Path to the NSS certificate database containing the certificates to
     * use to simulate a software smartcard
     *
     * Since: 0.7
     **/
    g_object_class_install_property
        (gobject_class, PROP_SMARTCARD_DB,
         g_param_spec_string("smartcard-db",
                              "Smartcard certificate database",
                              "Path to the database for smartcard certificates",
                              NULL,
                              G_PARAM_READABLE |
                              G_PARAM_WRITABLE |
                              G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:enable-usbredir:
     *
     * If set to TRUE, the usbredir channel will be enabled and USB devices
     * can be redirected to the guest
     *
     * Since: 0.8
     **/
    g_object_class_install_property
        (gobject_class, PROP_USBREDIR,
         g_param_spec_boolean("enable-usbredir",
                          "Enable USB device redirection",
                          "Forward USB devices to the SPICE server",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession::inhibit-keyboard-grab:
     *
     * This boolean is set by the usbredir channel to indicate to #SpiceDisplay
     * that the keyboard grab should be temporarily released, because it is
     * going to invoke policykit. It will get reset when the usbredir channel
     * is done with polickit.
     *
     * Since: 0.8
     **/
    g_object_class_install_property
        (gobject_class, PROP_INHIBIT_KEYBOARD_GRAB,
         g_param_spec_boolean("inhibit-keyboard-grab",
                        "Inhibit Keyboard Grab",
                        "Request that SpiceDisplays don't grab the keyboard",
                        FALSE,
                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:ca:
     *
     * CA certificates in PEM format. The text data can contain
     * several CA certificates identified by:
     *
     *  -----BEGIN CERTIFICATE-----
     *  ... (CA certificate in base64 encoding) ...
     *  -----END CERTIFICATE-----
     *
     * Since: 0.15
     **/
    g_object_class_install_property
        (gobject_class, PROP_CA,
         g_param_spec_boxed("ca",
                            "CA",
                            "The CA certificates data",
                            G_TYPE_BYTE_ARRAY,
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:secure-channels:
     *
     * A string array of channel types to be secured.
     *
     * Since: 0.20
     **/
    g_object_class_install_property
        (gobject_class, PROP_SECURE_CHANNELS,
         g_param_spec_boxed ("secure-channels",
                             "Secure channels",
                             "Array of channel type to secure",
                             G_TYPE_STRV,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));


    /**
     * SpiceSession::channel-new:
     * @session: the session that emitted the signal
     * @channel: the new #SpiceChannel
     *
     * The #SpiceSession::channel-new signal is emitted each time a #SpiceChannel is created.
     **/
    signals[SPICE_SESSION_CHANNEL_NEW] =
        g_signal_new("channel-new",
                     G_OBJECT_CLASS_TYPE(gobject_class),
                     G_SIGNAL_RUN_FIRST,
                     G_STRUCT_OFFSET(SpiceSessionClass, channel_new),
                     NULL, NULL,
                     g_cclosure_marshal_VOID__OBJECT,
                     G_TYPE_NONE,
                     1,
                     SPICE_TYPE_CHANNEL);

    /**
     * SpiceSession::channel-destroy:
     * @session: the session that emitted the signal
     * @channel: the destroyed #SpiceChannel
     *
     * The #SpiceSession::channel-destroy signal is emitted each time a #SpiceChannel is destroyed.
     **/
    signals[SPICE_SESSION_CHANNEL_DESTROY] =
        g_signal_new("channel-destroy",
                     G_OBJECT_CLASS_TYPE(gobject_class),
                     G_SIGNAL_RUN_FIRST,
                     G_STRUCT_OFFSET(SpiceSessionClass, channel_destroy),
                     NULL, NULL,
                     g_cclosure_marshal_VOID__OBJECT,
                     G_TYPE_NONE,
                     1,
                     SPICE_TYPE_CHANNEL);

    /**
     * SpiceSession::disconnected:
     * @session: the session that emitted the signal
     *
     * The #SpiceSession::disconnected signal is emitted when all channels have been destroyed.
     * Since: 0.35
     **/
    signals[SPICE_SESSION_DISCONNECTED] =
        g_signal_new("disconnected",
                     G_OBJECT_CLASS_TYPE(gobject_class),
                     G_SIGNAL_RUN_FIRST,
                     0, NULL, NULL,
                     g_cclosure_marshal_VOID__VOID,
                     G_TYPE_NONE,
                     0,
                     NULL);

    /**
     * SpiceSession::mm-time-reset:
     * @session: the session that emitted the signal
     *
     * The #SpiceSession::mm-time-reset is emitted when we identify discontinuity in mm-time
     *
     * Since 0.20
     **/
    signals[SPICE_SESSION_MM_TIME_RESET] =
        g_signal_new("mm-time-reset",
                     G_OBJECT_CLASS_TYPE(gobject_class),
                     G_SIGNAL_RUN_FIRST,
                     0, NULL, NULL,
                     g_cclosure_marshal_VOID__VOID,
                     G_TYPE_NONE,
                     0);

    /**
     * SpiceSession:read-only:
     *
     * Whether this connection is read-only mode.
     *
     * Since: 0.8
     **/
    g_object_class_install_property
        (gobject_class, PROP_READ_ONLY,
         g_param_spec_boolean("read-only", "Read-only",
                              "Whether this connection is read-only mode",
                              FALSE,
                              G_PARAM_READWRITE |
                              G_PARAM_CONSTRUCT |
                              G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:cache-size:
     *
     * Images cache size. If 0, don't set.
     *
     * Since: 0.9
     **/
    g_object_class_install_property
        (gobject_class, PROP_CACHE_SIZE,
         g_param_spec_int("cache-size",
                          "Cache size",
                          "Images cache size (bytes)",
                          0, G_MAXINT, 0,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:glz-window-size:
     *
     * Glz window size. If 0, don't set.
     *
     * Since: 0.9
     **/
    g_object_class_install_property
        (gobject_class, PROP_GLZ_WINDOW_SIZE,
         g_param_spec_int("glz-window-size",
                          "Glz window size",
                          "Glz window size (bytes)",
                          0, LZ_MAX_WINDOW_SIZE * 4, 0,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:name:
     *
     * Spice server name.
     *
     * Since: 0.11
     **/
    g_object_class_install_property
        (gobject_class, PROP_NAME,
         g_param_spec_string("name",
                             "Name",
                             "Spice server name",
                             NULL,
                             G_PARAM_READABLE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:uuid:
     *
     * Spice server uuid.
     *
     * Since: 0.11
     **/
    g_object_class_install_property
        (gobject_class, PROP_UUID,
         g_param_spec_pointer("uuid",
                              "UUID",
                              "Spice server uuid",
                              G_PARAM_READABLE |
                              G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:proxy:
     *
     * URI to the proxy server to use when doing network connection.
     * of the form <![CDATA[ [protocol://]<host>[:port] ]]>
     *
     * Since: 0.17
     **/
    g_object_class_install_property
        (gobject_class, PROP_PROXY,
         g_param_spec_string("proxy",
                             "Proxy",
                             "The proxy server",
                             NULL,
                             G_PARAM_READWRITE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:shared-dir:
     *
     * Location of the shared directory
     *
     * Since: 0.24
     **/
    g_object_class_install_property
        (gobject_class, PROP_SHARED_DIR,
         g_param_spec_string("shared-dir",
                             "Shared directory",
                             "Shared directory",
                             g_get_user_special_dir(G_USER_DIRECTORY_PUBLIC_SHARE),
                             G_PARAM_READWRITE |
                             G_PARAM_CONSTRUCT |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:share-dir-ro:
     *
     * Whether to share the directory read-only.
     *
     * Since: 0.28
     **/
    g_object_class_install_property
        (gobject_class, PROP_SHARE_DIR_RO,
         g_param_spec_boolean("share-dir-ro",
                              "Share directory read-only",
                              "Share directory read-only",
                              FALSE,
                              G_PARAM_READWRITE |
                              G_PARAM_CONSTRUCT |
                              G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:webdav-server:
     *
     * Phodav server that is internally used by #SpiceSession to make
     * #SpiceSession:shared-dir available to the host.
     *
     * Since: 0.39
     **/
    g_object_class_install_property
        (gobject_class, PROP_WEBDAV_SERVER,
         g_param_spec_object("webdav-server",
                             "WebDAV server",
                             "PhodavServer object used for directory sharing",
#ifdef USE_PHODAV
                             PHODAV_TYPE_SERVER,
#else
                             G_TYPE_OBJECT,
#endif
                             G_PARAM_READABLE |
                             G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:preferred-compression:
     *
     * The image compression algorithm the client prefers to use. It is
     * reported to the server.
     *
     * Since: 0.29
     **/
    g_object_class_install_property
        (gobject_class, PROP_PREF_COMPRESSION,
         g_param_spec_enum("preferred-compression",
                           "Preferred image compression algorithm",
                           "Preferred image compression algorithm",
                           SPICE_TYPE_IMAGE_COMPRESSION,
                           SPICE_IMAGE_COMPRESSION_INVALID,
                           G_PARAM_READWRITE |
                           G_PARAM_STATIC_STRINGS));

    /**
     * SpiceSession:gl-scanout:
     *
     * Whether to enable gl-scanout (Unix only).  Set to TRUE by
     * default on EGL-enabled host, unless SPICE_DISABLE_GL_SCANOUT
     * environment variable is set.
     *
     * Since: 0.36
     **/
    g_object_class_install_property
        (gobject_class, PROP_GL_SCANOUT,
         g_param_spec_boolean("gl-scanout",
                              "Enable GL scanout support",
                              "Enable GL scanout support",
#ifdef HAVE_EGL
                              g_getenv("SPICE_DISABLE_GL_SCANOUT") == NULL,
                              G_PARAM_CONSTRUCT |
#else
                              false,
#endif
                              G_PARAM_READWRITE |
                              G_PARAM_STATIC_STRINGS));
}

G_GNUC_INTERNAL
gboolean spice_session_get_gl_scanout_enabled(SpiceSession *session)
{
    return session->priv->gl_scanout;
}

/* ------------------------------------------------------------------ */
/* public functions                                                   */

/**
 * spice_session_new:
 *
 * Creates a new Spice session.
 *
 * Returns: a new #SpiceSession
 **/
SpiceSession *spice_session_new(void)
{
    SpiceSession *self = SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION, NULL));
    SpiceSessionPrivate *priv = self->priv;
    GError *err = NULL;

    priv->usb_manager = spice_usb_device_manager_get(self, &err);
    if (err != NULL) {
        SPICE_DEBUG("Could not initialize SpiceUsbDeviceManager - %s", err->message);
        g_clear_error(&err);
    }

    return self;
}

G_GNUC_INTERNAL
SpiceSession *spice_session_new_from_session(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;
    SpiceSession *copy;
    SpiceSessionPrivate *c;

    if (s->client_provided_sockets) {
        g_warning("migration with client provided fd is not supported yet");
        return NULL;
    }

    copy = SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION,
                                      "host", NULL,
                                      "ca-file", NULL,
                                      NULL));
    c = copy->priv;
    g_clear_object(&c->proxy);

    g_warn_if_fail(c->host == NULL);
    g_warn_if_fail(c->unix_path == NULL);
    g_warn_if_fail(c->tls_port == NULL);
    g_warn_if_fail(c->username == NULL);
    g_warn_if_fail(c->password == NULL);
    g_warn_if_fail(c->ca_file == NULL);
    g_warn_if_fail(c->ciphers == NULL);
    g_warn_if_fail(c->cert_subject == NULL);
    g_warn_if_fail(c->pubkey == NULL);
    g_warn_if_fail(c->pubkey == NULL);
    g_warn_if_fail(c->proxy == NULL);

    g_object_get(session,
                 "host", &c->host,
                 "unix-path", &c->unix_path,
                 "tls-port", &c->tls_port,
                 "username", &c->username,
                 "password", &c->password,
                 "ca-file", &c->ca_file,
                 "ciphers", &c->ciphers,
                 "cert-subject", &c->cert_subject,
                 "pubkey", &c->pubkey,
                 "verify", &c->verify,
                 "smartcard-certificates", &c->smartcard_certificates,
                 "smartcard-db", &c->smartcard_db,
                 "enable-smartcard", &c->smartcard,
                 "enable-audio", &c->audio,
                 "enable-usbredir", &c->usbredir,
                 "ca", &c->ca,
                 NULL);

    c->client_provided_sockets = s->client_provided_sockets;
    c->protocol = s->protocol;
    c->connection_id = s->connection_id;
    if (s->proxy)
        c->proxy = g_object_ref(s->proxy);

    return copy;
}

/**
 * spice_session_connect:
 * @session: a #SpiceSession
 *
 * Open the session using the #SpiceSession:host and
 * #SpiceSession:port.
 *
 * Returns: %FALSE if the session state is invalid for connection
 * request. %TRUE if the connection is initiated. To know whether the
 * connection is established, you must watch for channels creation
 * (#SpiceSession::channel-new) and the channels state
 * (#SpiceChannel::channel-event).
 **/
gboolean spice_session_connect(SpiceSession *session)
{
    SpiceSessionPrivate *s;

    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    s = session->priv;
    g_return_val_if_fail(!s->disconnecting, FALSE);

    session_disconnect(session, TRUE);

    s->client_provided_sockets = FALSE;

    if (s->cmain == NULL)
        s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);

    glz_decoder_window_clear(s->glz_window);
    return spice_channel_connect(s->cmain);
}

/**
 * spice_session_open_fd:
 * @session: a #SpiceSession
 * @fd: a file descriptor (socket) or -1
 *
 * Open the session using the provided @fd socket file
 * descriptor. This is useful if you create the fd yourself, for
 * example to setup a SSH tunnel.
 *
 * Note however that additional sockets will be needed by all the channels
 * created for @session so users of this API should hook into
 * SpiceChannel::open-fd signal for each channel they are interested in, and
 * create and pass a new socket to the channel using #spice_channel_open_fd, in
 * the signal callback.
 *
 * If @fd is -1, a valid fd will be requested later via the
 * SpiceChannel::open-fd signal. Typically, you would want to just pass -1 as
 * @fd this call since you will have to hook to SpiceChannel::open-fd signal
 * anyway.
 *
 * Returns: %TRUE on success.
 **/
gboolean spice_session_open_fd(SpiceSession *session, int fd)
{
    SpiceSessionPrivate *s;

    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
    g_return_val_if_fail(fd >= -1, FALSE);

    s = session->priv;
    g_return_val_if_fail(!s->disconnecting, FALSE);

    session_disconnect(session, TRUE);

    s->client_provided_sockets = TRUE;

    if (s->cmain == NULL)
        s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);

    glz_decoder_window_clear(s->glz_window);
    return spice_channel_open_fd(s->cmain, fd);
}

G_GNUC_INTERNAL
gboolean spice_session_get_client_provided_socket(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    SpiceSessionPrivate *s = session->priv;

    return s->client_provided_sockets;
}

static void cache_clear_all(SpiceSession *self)
{
    SpiceSessionPrivate *s = self->priv;

    cache_clear(s->images);
    glz_decoder_window_clear(s->glz_window);
}

G_GNUC_INTERNAL
void spice_session_switching_disconnect(SpiceSession *self)
{
    g_return_if_fail(SPICE_IS_SESSION(self));

    SpiceSessionPrivate *s = self->priv;

    g_return_if_fail(s->cmain != NULL);

    /* disconnect/destroy all but main channel */

    for (GList *l = s->channels; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        if (channel == s->cmain)
            continue;
        spice_session_channel_destroy(self, channel);
    }

    g_warn_if_fail(s->channels != NULL);

    cache_clear_all(self);
    s->connection_id = 0;
}

#define SWAP_STR(x, y) G_STMT_START { \
    gchar *tmp = x;                   \
    x = y;                            \
    y = tmp;                          \
} G_STMT_END

G_GNUC_INTERNAL
void spice_session_start_migrating(SpiceSession *session,
                                   gboolean full_migration)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;
    SpiceSessionPrivate *m;

    g_return_if_fail(s->migration != NULL);
    m = s->migration->priv;
    g_return_if_fail(m->migration_state == SPICE_SESSION_MIGRATION_CONNECTING);


    s->full_migration = full_migration;
    spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_MIGRATING);

    /* swapping connection details happens after MIGRATION_CONNECTING state */
    SWAP_STR(s->host, m->host);
    SWAP_STR(s->port, m->port);
    SWAP_STR(s->tls_port, m->tls_port);
    SWAP_STR(s->unix_path, m->unix_path);

    g_warn_if_fail(g_list_length(s->channels) == g_list_length(m->channels));

    SPICE_DEBUG("migration channels left:%u (in migration:%u)",
                g_list_length(s->channels), g_list_length(m->channels));
    s->migration_left = spice_session_get_channels(session);
}
#undef SWAP_STR

G_GNUC_INTERNAL
SpiceChannel* spice_session_lookup_channel(SpiceSession *session, gint id, gint type)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;
    SpiceChannel *channel = NULL;

    for (GList *l = s->channels; l != NULL; ) {
        channel = l->data;
        l = l->next;

        if (id == spice_channel_get_channel_id(channel) &&
            type == spice_channel_get_channel_type(channel)) {
            break;
        }
    }
    g_return_val_if_fail(channel != NULL, NULL);

    return channel;
}

G_GNUC_INTERNAL
void spice_session_abort_migration(SpiceSession *session)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    if (s->migration == NULL) {
        SPICE_DEBUG("no migration in progress");
        return;
    }

    SPICE_DEBUG("migration: abort");
    if (s->migration_state != SPICE_SESSION_MIGRATION_MIGRATING)
        goto end;

    for (GList *l = s->channels; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        if (g_list_find(s->migration_left, channel))
            continue;

        spice_channel_swap(channel,
            spice_session_lookup_channel(s->migration,
                                         spice_channel_get_channel_id(channel),
                                         spice_channel_get_channel_type(channel)),
                                         !s->full_migration);
    }

end:
    g_clear_pointer(&s->migration_left, g_list_free);
    session_disconnect(s->migration, FALSE);
    g_clear_object(&s->migration);

    s->migrate_wait_init = FALSE;
    if (s->after_main_init) {
        g_source_remove(s->after_main_init);
        s->after_main_init = 0;
    }

    spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
}

G_GNUC_INTERNAL
void spice_session_channel_migrate(SpiceSession *session, SpiceChannel *channel)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;
    SpiceChannel *c;
    gint id, type;

    g_return_if_fail(s->migration != NULL);
    g_return_if_fail(SPICE_IS_CHANNEL(channel));

    id = spice_channel_get_channel_id(channel);
    type = spice_channel_get_channel_type(channel);
    CHANNEL_DEBUG(channel, "migrating channel id:%d type:%d", id, type);

    c = spice_session_lookup_channel(s->migration, id, type);
    g_return_if_fail(c != NULL);

    if (!g_queue_is_empty(&c->priv->xmit_queue) && s->full_migration) {
        CHANNEL_DEBUG(channel, "mig channel xmit queue is not empty. type %s", c->priv->name);
    }
    spice_channel_swap(channel, c, !s->full_migration);
    s->migration_left = g_list_remove(s->migration_left, channel);

    if (g_list_length(s->migration_left) == 0) {
        CHANNEL_DEBUG(channel, "migration: all channel migrated, success");
        session_disconnect(s->migration, FALSE);
        g_clear_object(&s->migration);
        spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
    }
}

/* main context */
static gboolean after_main_init(gpointer data)
{
    SpiceSession *self = data;
    SpiceSessionPrivate *s = self->priv;
    GList *l;

    for (l = s->migration_left; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        spice_session_channel_migrate(self, channel);
        channel->priv->state = SPICE_CHANNEL_STATE_READY;
        spice_channel_up(channel);
    }

    s->after_main_init = 0;
    return FALSE;
}

/* coroutine context */
G_GNUC_INTERNAL
gboolean spice_session_migrate_after_main_init(SpiceSession *self)
{
    g_return_val_if_fail(SPICE_IS_SESSION(self), FALSE);

    SpiceSessionPrivate *s = self->priv;

    if (!s->migrate_wait_init)
        return FALSE;

    g_return_val_if_fail(g_list_length(s->migration_left) != 0, FALSE);
    g_return_val_if_fail(s->after_main_init == 0, FALSE);

    s->migrate_wait_init = FALSE;
    s->after_main_init = g_idle_add(after_main_init, self);

    return TRUE;
}

/* main context */
G_GNUC_INTERNAL
void spice_session_migrate_end(SpiceSession *self)
{
    g_return_if_fail(SPICE_IS_SESSION(self));

    SpiceSessionPrivate *s = self->priv;
    SpiceMsgOut *out;

    g_return_if_fail(s->migration);
    g_return_if_fail(s->migration->priv->cmain);
    g_return_if_fail(g_list_length(s->migration_left) != 0);

    /* disconnect and reset all channels */
    for (GList *l = s->migration_left; l != NULL; ) {
        SpiceChannel *channel = l->data;
        l = l->next;

        if (!SPICE_IS_MAIN_CHANNEL(channel)) {
            /* freeze other channels */
            channel->priv->state = SPICE_CHANNEL_STATE_MIGRATING;
        }

        /* reset for migration, disconnect */
        spice_channel_reset(channel, TRUE);

        if (SPICE_IS_MAIN_CHANNEL(channel)) {
            /* migrate main to target, so we can start talking */
            spice_session_channel_migrate(self, channel);
        }
    }

    cache_clear_all(self);

    /* send MIGRATE_END to target */
    out = spice_msg_out_new(s->cmain, SPICE_MSGC_MAIN_MIGRATE_END);
    spice_msg_out_send(out);

    /* now wait after main init for the rest of channels migration */
    s->migrate_wait_init = TRUE;
}

/**
 * spice_session_get_read_only:
 * @session: a #SpiceSession
 *
 * Checks whether the @session is read-only.
 *
 * Returns: whether the @session is in read-only mode.
 **/
gboolean spice_session_get_read_only(SpiceSession *self)
{
    g_return_val_if_fail(SPICE_IS_SESSION(self), FALSE);

    return self->priv->read_only;
}

static gboolean session_disconnect_idle(SpiceSession *self)
{
    SpiceSessionPrivate *s = self->priv;

    session_disconnect(self, FALSE);
    s->disconnecting = 0;

    g_object_unref(self);

    return FALSE;
}

/**
 * spice_session_disconnect:
 * @session: a #SpiceSession
 *
 * Disconnect the @session, and destroy all channels.
 **/
void spice_session_disconnect(SpiceSession *session)
{
    SpiceSessionPrivate *s;

    g_return_if_fail(SPICE_IS_SESSION(session));

    s = session->priv;

    SPICE_DEBUG("session: disconnecting %u", s->disconnecting);
    if (s->disconnecting != 0)
        return;

    g_object_ref(session);
    s->disconnecting = g_idle_add((GSourceFunc)session_disconnect_idle, session);
}

/**
 * spice_session_get_channels:
 * @session: a #SpiceSession
 *
 * Get the list of current channels associated with this @session.
 *
 * Returns: (element-type SpiceChannel) (transfer container): a #GList
 *          of unowned #SpiceChannel channels.
 **/
GList *spice_session_get_channels(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
    g_return_val_if_fail(session->priv != NULL, NULL);

    return g_list_copy(session->priv->channels);
}

/**
 * spice_session_has_channel_type:
 * @session: a #SpiceSession
 * @type: a #SpiceChannel:channel-type
 *
 * See if there is a @type channel in the channels associated with this
 * @session.
 *
 * Returns: TRUE if a @type channel is available otherwise FALSE.
 **/
gboolean spice_session_has_channel_type(SpiceSession *session, gint type)
{
    SpiceSessionPrivate *s;

    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
    g_return_val_if_fail(session->priv != NULL, FALSE);

    s = session->priv;

    for (GList *l = s->channels; l != NULL; l = l->next) {
        SpiceChannel *channel = l->data;
        if (spice_channel_get_channel_type(channel) == type) {
            return TRUE;
        }
    }
    return FALSE;
}

/* ------------------------------------------------------------------ */
/* private functions                                                  */

typedef struct spice_open_host spice_open_host;

struct spice_open_host {
    struct coroutine *from;
    SpiceSession *session;
    SpiceChannel *channel;
    SpiceURI *proxy;
    int port;
    GCancellable *cancellable;
    GError *error;
    GSocketConnection *connection;
    GSocketClient *client;
};

static void socket_client_connect_ready(GObject *source_object, GAsyncResult *result,
                                        gpointer data)
{
    GSocketClient *client = G_SOCKET_CLIENT(source_object);
    spice_open_host *open_host = data;
    GSocketConnection *connection = NULL;

    CHANNEL_DEBUG(open_host->channel, "connect ready");
    connection = g_socket_client_connect_finish(client, result, &open_host->error);
    if (connection == NULL) {
        g_warn_if_fail(open_host->error != NULL);
        goto end;
    }

    open_host->connection = connection;

end:
    coroutine_yieldto(open_host->from, NULL);
}

/* main context */
static void open_host_connectable_connect(spice_open_host *open_host, GSocketConnectable *connectable)
{
    CHANNEL_DEBUG(open_host->channel, "connecting %p...", open_host);

    g_socket_client_connect_async(open_host->client, connectable,
                                  open_host->cancellable,
                                  socket_client_connect_ready, open_host);
}

/* main context */
static void proxy_lookup_ready(GObject *source_object, GAsyncResult *result,
                               gpointer data)
{
    spice_open_host *open_host = data;
    SpiceSession *session = open_host->session;
    SpiceSessionPrivate *s = session->priv;
    GList *addresses = NULL, *it;
    GSocketAddress *address;

    SPICE_DEBUG("proxy lookup ready");
    addresses = g_resolver_lookup_by_name_finish(G_RESOLVER(source_object),
                                                 result, &open_host->error);
    if (addresses == NULL || open_host->error) {
        g_prefix_error(&open_host->error, "SPICE proxy: ");
        coroutine_yieldto(open_host->from, NULL);
        return;
    }

    for (it = addresses; it != NULL; it = it->next) {
        address = g_proxy_address_new(G_INET_ADDRESS(it->data),
                                      spice_uri_get_port(open_host->proxy),
                                      spice_uri_get_scheme(open_host->proxy),
                                      s->host, open_host->port,
                                      spice_uri_get_user(open_host->proxy),
                                      spice_uri_get_password(open_host->proxy));
        if (address != NULL)
            break;
    }

    open_host_connectable_connect(open_host, G_SOCKET_CONNECTABLE(address));
    g_resolver_free_addresses(addresses);
    g_object_unref(address);
}

/* main context */
static gboolean open_host_idle_cb(gpointer data)
{
    spice_open_host *open_host = data;
    SpiceSessionPrivate *s;

    g_return_val_if_fail(open_host != NULL, FALSE);
    g_return_val_if_fail(open_host->connection == NULL, FALSE);

    if (spice_channel_get_session(open_host->channel) != open_host->session)
        return FALSE;

    s = open_host->session->priv;
    open_host->proxy = s->proxy;
    if (open_host->error != NULL) {
        coroutine_yieldto(open_host->from, NULL);
        return FALSE;
    }

    if (open_host->proxy) {
        g_resolver_lookup_by_name_async(g_resolver_get_default(),
                                        spice_uri_get_hostname(open_host->proxy),
                                        open_host->cancellable,
                                        proxy_lookup_ready, open_host);
    } else {
        GSocketConnectable *address = NULL;

        if (s->unix_path) {
            SPICE_DEBUG("open unix path %s", s->unix_path);
#ifdef G_OS_UNIX
            address = G_SOCKET_CONNECTABLE(g_unix_socket_address_new(s->unix_path));
#else
            g_set_error_literal(&open_host->error, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
                                "Unix path unsupported on this platform");
#endif
        } else {
            SPICE_DEBUG("open host %s:%d", s->host, open_host->port);
            address = g_network_address_parse(s->host, open_host->port, &open_host->error);
        }

        if (address == NULL || open_host->error != NULL) {
            coroutine_yieldto(open_host->from, NULL);
            return FALSE;
        }

        open_host_connectable_connect(open_host, address);
        g_object_unref(address);
    }

    if (open_host->proxy != NULL) {
        gchar *str = spice_uri_to_string(open_host->proxy);
        SPICE_DEBUG("(with proxy %s)", str);
        g_free(str);
    }

    return FALSE;
}

#define SOCKET_TIMEOUT 10

/* coroutine context */
G_GNUC_INTERNAL
GSocketConnection* spice_session_channel_open_host(SpiceSession *session, SpiceChannel *channel,
                                                   gboolean *use_tls, GError **error)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;
    SpiceChannelPrivate *c = channel->priv;
    spice_open_host open_host = { 0, };
    gchar *port, *endptr;

    // FIXME: make open_host() cancellable
    open_host.from = coroutine_self();
    open_host.session = session;
    open_host.channel = channel;

    const char *name = spice_channel_type_to_string(c->channel_type);
    if (spice_strv_contains(s->secure_channels, "all") ||
        spice_strv_contains(s->secure_channels, name))
        *use_tls = TRUE;

    if (s->unix_path) {
        if (*use_tls) {
            CHANNEL_DEBUG(channel, "No TLS for Unix sockets");
            return NULL;
        }
    } else {
        port = *use_tls ? s->tls_port : s->port;
        if (port == NULL) {
            SPICE_DEBUG("Missing port value, not attempting %s connection.",
                    *use_tls?"TLS":"unencrypted");
            return NULL;
        }

        open_host.port = strtol(port, &endptr, 10);
        if (*port == '\0' || *endptr != '\0' ||
            open_host.port <= 0 || open_host.port > G_MAXUINT16) {
            g_warning("Invalid port value %s", port);
            return NULL;
        }
    }
    if (*use_tls) {
        CHANNEL_DEBUG(channel, "Using TLS, port %d", open_host.port);
    } else {
        CHANNEL_DEBUG(channel, "Using plain text, port %d", open_host.port);
    }

    open_host.client = g_socket_client_new();
    g_socket_client_set_enable_proxy(open_host.client, s->proxy != NULL);
    g_socket_client_set_timeout(open_host.client, SOCKET_TIMEOUT);

    g_idle_add(open_host_idle_cb, &open_host);
    /* switch to main loop and wait for connection */
    coroutine_yield(NULL);

    if (open_host.error != NULL) {
        CHANNEL_DEBUG(channel, "open host: %s", open_host.error->message);
        g_propagate_error(error, open_host.error);
    } else if (open_host.connection != NULL) {
        GSocket *socket;
        socket = g_socket_connection_get_socket(open_host.connection);
        g_socket_set_timeout(socket, 0);
        g_socket_set_blocking(socket, FALSE);
        g_socket_set_keepalive(socket, TRUE);

        /* Make client timeouts a bit more responsive */
#if defined(_WIN32)
        /*  Windows does not support setting count */
        struct tcp_keepalive keepalive = {
            TRUE,
            30 * 1000,
            5 * 1000
        };
        DWORD written;
        WSAIoctl(g_socket_get_fd(socket), SIO_KEEPALIVE_VALS, &keepalive, sizeof(keepalive),
                 NULL, 0, &written, NULL, NULL);
#elif defined(TCP_KEEPIDLE) && defined(TCP_KEEPINTVL)
        g_socket_set_option(socket, SOL_TCP, TCP_KEEPIDLE, 30, NULL);
        g_socket_set_option(socket, SOL_TCP, TCP_KEEPINTVL, 15, NULL);
        g_socket_set_option(socket, SOL_TCP, TCP_KEEPCNT, 3, NULL);
#endif
    }

    g_clear_object(&open_host.client);
    return open_host.connection;
}


G_GNUC_INTERNAL
void spice_session_channel_new(SpiceSession *session, SpiceChannel *channel)
{
    g_return_if_fail(SPICE_IS_SESSION(session));
    g_return_if_fail(SPICE_IS_CHANNEL(channel));

    SpiceSessionPrivate *s = session->priv;

    s->channels = g_list_prepend(s->channels, channel);

    if (SPICE_IS_MAIN_CHANNEL(channel)) {
        gboolean all = spice_strv_contains(s->disable_effects, "all");

        g_object_set(channel,
                     "disable-wallpaper", all || spice_strv_contains(s->disable_effects, "wallpaper"),
                     "disable-font-smooth", all || spice_strv_contains(s->disable_effects, "font-smooth"),
                     "disable-animation", all || spice_strv_contains(s->disable_effects, "animation"),
                     NULL);

        CHANNEL_DEBUG(channel, "new main channel, switching");
        s->cmain = channel;
    } else if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
        g_warn_if_fail(s->playback_channel == NULL);
        s->playback_channel = SPICE_PLAYBACK_CHANNEL(channel);
    }

    g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
}

static void channel_finally_destroyed(gpointer data, GObject *channel)
{
    SpiceSession *session = SPICE_SESSION(data);
    SpiceSessionPrivate *s = session->priv;
    s->channels_destroying--;
    if (s->channels == NULL && (s->channels_destroying == 0)) {
        g_signal_emit(session, signals[SPICE_SESSION_DISCONNECTED], 0);
    }
    g_object_unref(session);
}

static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel)
{
    g_return_if_fail(SPICE_IS_SESSION(session));
    g_return_if_fail(SPICE_IS_CHANNEL(channel));

    SpiceSessionPrivate *s = session->priv;
    GList *l;

    if (s->migration_left)
        s->migration_left = g_list_remove(s->migration_left, channel);

    for (l = s->channels; l != NULL; l = l->next) {
        if (l->data == channel)
            break;
    }

    g_return_if_fail(l != NULL);

    if (channel == s->cmain) {
        CHANNEL_DEBUG(channel, "the session lost the main channel");
        s->cmain = NULL;
    }

    s->channels = g_list_delete_link(s->channels, l);

    g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_DESTROY], 0, channel);

    g_clear_object(&channel->priv->session);
    spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);

    /* Wait until the channel is properly freed so that we can emit a
     * 'disconnected' signal */
    s->channels_destroying++;
    g_object_weak_ref(G_OBJECT(channel), channel_finally_destroyed, g_object_ref(session));

    g_object_unref(channel);
}

G_GNUC_INTERNAL
void spice_session_set_connection_id(SpiceSession *session, int id)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    s->connection_id = id;
}

G_GNUC_INTERNAL
int spice_session_get_connection_id(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), -1);

    SpiceSessionPrivate *s = session->priv;

    return s->connection_id;
}

G_GNUC_INTERNAL
guint32 spice_session_get_mm_time(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), 0);

    SpiceSessionPrivate *s = session->priv;

    /* FIXME: we may want to estimate the drift of clocks, and well,
       do something better than this trivial approach */
    return (g_get_monotonic_time() - s->mm_time_offset) / 1000;
}

#define MM_TIME_DIFF_RESET_THRESH 500 // 0.5 sec

G_GNUC_INTERNAL
void spice_session_set_mm_time(SpiceSession *session, guint32 time)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;
    guint32 old_time;

    old_time = spice_session_get_mm_time(session);

    s->mm_time_offset = g_get_monotonic_time() - time * (guint64) 1000;
    SPICE_DEBUG("set mm time: %u", time);
    if (spice_mmtime_diff(time, old_time + MM_TIME_DIFF_RESET_THRESH) > 0 ||
        spice_mmtime_diff(time, old_time) < 0) {
        SPICE_DEBUG("%s: mm-time-reset, old %u, new %u", __FUNCTION__, old_time, time);
        g_coroutine_signal_emit(session, signals[SPICE_SESSION_MM_TIME_RESET], 0);
    }
}

G_GNUC_INTERNAL
void spice_session_set_port(SpiceSession *session, int port, gboolean tls)
{
    const char *prop = tls ? "tls-port" : "port";
    char *tmp;

    g_return_if_fail(SPICE_IS_SESSION(session));

    /* old spicec client doesn't accept port == 0, see Migrate::start */
    tmp = port > 0 ? g_strdup_printf("%d", port) : NULL;
    g_object_set(session, prop, tmp, NULL);
    g_free(tmp);
}

G_GNUC_INTERNAL
void spice_session_get_pubkey(SpiceSession *session, guint8 **pubkey, guint *size)
{
    g_return_if_fail(SPICE_IS_SESSION(session));
    g_return_if_fail(pubkey != NULL);
    g_return_if_fail(size != NULL);

    SpiceSessionPrivate *s = session->priv;

    *pubkey = s->pubkey ? s->pubkey->data : NULL;
    *size = s->pubkey ? s->pubkey->len : 0;
}

G_GNUC_INTERNAL
void spice_session_get_ca(SpiceSession *session, guint8 **ca, guint *size)
{
    g_return_if_fail(SPICE_IS_SESSION(session));
    g_return_if_fail(ca != NULL);
    g_return_if_fail(size != NULL);

    SpiceSessionPrivate *s = session->priv;

    *ca = s->ca ? s->ca->data : NULL;
    *size = s->ca ? s->ca->len : 0;
}

G_GNUC_INTERNAL
guint spice_session_get_verify(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), 0);

    SpiceSessionPrivate *s = session->priv;

    return s->verify;
}

G_GNUC_INTERNAL
void spice_session_set_migration_state(SpiceSession *session, SpiceSessionMigration state)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    if (state == SPICE_SESSION_MIGRATION_CONNECTING)
        s->for_migration = true;

    s->migration_state = state;
    g_coroutine_object_notify(G_OBJECT(session), "migration-state");
}

G_GNUC_INTERNAL
const gchar* spice_session_get_username(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->username;
}

G_GNUC_INTERNAL
const gchar* spice_session_get_password(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->password;
}

G_GNUC_INTERNAL
const gchar* spice_session_get_host(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->host;
}

G_GNUC_INTERNAL
const gchar* spice_session_get_cert_subject(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->cert_subject;
}

G_GNUC_INTERNAL
const gchar* spice_session_get_ciphers(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->ciphers;
}

G_GNUC_INTERNAL
const gchar* spice_session_get_ca_file(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->ca_file;
}

G_GNUC_INTERNAL
void spice_session_get_caches(SpiceSession *session,
                              display_cache **images,
                              SpiceGlzDecoderWindow **glz_window)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    if (images)
        *images = s->images;
    if (glz_window)
        *glz_window = s->glz_window;
}

G_GNUC_INTERNAL
void spice_session_set_caches_hints(SpiceSession *session,
                                    uint32_t pci_ram_size,
                                    uint32_t n_display_channels)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    s->n_display_channels = n_display_channels;

    /* TODO: when setting cache and window size, we should consider the client's
     *       available memory and the number of display channels */
    if (s->images_cache_size == 0) {
        s->images_cache_size = IMAGES_CACHE_SIZE_DEFAULT;
    }

    if (s->glz_window_size == 0) {
        s->glz_window_size = MIN(MAX_GLZ_WINDOW_SIZE_DEFAULT, pci_ram_size / 2);
        s->glz_window_size = MAX(MIN_GLZ_WINDOW_SIZE_DEFAULT, s->glz_window_size);
    }
}

G_GNUC_INTERNAL
guint spice_session_get_n_display_channels(SpiceSession *session)
{
    g_return_val_if_fail(session != NULL, 0);

    return session->priv->n_display_channels;
}

G_GNUC_INTERNAL
void spice_session_set_uuid(SpiceSession *session, guint8 uuid[16])
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    memcpy(s->uuid, uuid, sizeof(s->uuid));

    g_coroutine_object_notify(G_OBJECT(session), "uuid");
}

G_GNUC_INTERNAL
void spice_session_set_name(SpiceSession *session, const gchar *name)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    g_free(s->name);
    s->name = g_strdup(name);

    g_coroutine_object_notify(G_OBJECT(session), "name");
}

G_GNUC_INTERNAL
void spice_session_sync_playback_latency(SpiceSession *session)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    if (s->playback_channel &&
        spice_playback_channel_is_active(s->playback_channel)) {
        spice_playback_channel_sync_latency(s->playback_channel);
    } else {
        SPICE_DEBUG("%s: not implemented when there isn't audio playback", __FUNCTION__);
    }
}

G_GNUC_INTERNAL
gboolean spice_session_is_playback_active(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    SpiceSessionPrivate *s = session->priv;

    return (s->playback_channel &&
        spice_playback_channel_is_active(s->playback_channel));
}

G_GNUC_INTERNAL
guint32 spice_session_get_playback_latency(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), 0);

    SpiceSessionPrivate *s = session->priv;

    if (s->playback_channel &&
        spice_playback_channel_is_active(s->playback_channel)) {
        return spice_playback_channel_get_latency(s->playback_channel);
    } else {
        SPICE_DEBUG("%s: not implemented when there isn't audio playback", __FUNCTION__);
        return 0;
    }
}

static const gchar* spice_session_get_shared_dir(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    SpiceSessionPrivate *s = session->priv;

    return s->shared_dir;
}

static void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir)
{
    g_return_if_fail(SPICE_IS_SESSION(session));

    SpiceSessionPrivate *s = session->priv;

    g_free(s->shared_dir);
    s->shared_dir = g_strdup(dir);

#ifdef HAVE_PHODAV_VIRTUAL
    if (s->webdav == NULL) {
        return;
    }

    PhodavVirtualDir *root;
    g_object_get(s->webdav, "root-file", &root, NULL);
    phodav_virtual_dir_root_set_real(root, s->shared_dir);
    g_object_unref(root);
#endif
}

G_GNUC_INTERNAL
const gchar* spice_audio_data_mode_to_string(gint mode)
{
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    static const char *str[] = {
        [ SPICE_AUDIO_DATA_MODE_INVALID ] = "invalid",
        [ SPICE_AUDIO_DATA_MODE_RAW ] = "raw",
        [ SPICE_AUDIO_DATA_MODE_CELT_0_5_1 ] = "celt",
        [ SPICE_AUDIO_DATA_MODE_OPUS ] = "opus",
    };
    G_GNUC_END_IGNORE_DEPRECATIONS
    return (mode >= 0 && mode < G_N_ELEMENTS(str)) ? str[mode] : "unknown audio codec";
}


/**
 * spice_session_get_proxy_uri:
 * @session: a #SpiceSession
 *
 * Gets the @session proxy uri.
 *
 * Returns: (transfer none): the session proxy #SpiceURI or %NULL.
 * Since: 0.24
 **/
SpiceURI *spice_session_get_proxy_uri(SpiceSession *session)
{
    SpiceSessionPrivate *s;

    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
    g_return_val_if_fail(session->priv != NULL, NULL);

    s = session->priv;

    return s->proxy;
}

/**
 * spice_audio_get:
 * @session: the #SpiceSession to connect to
 * @context: (allow-none): a #GMainContext to attach to (or %NULL for default).
 *
 * Gets the #SpiceAudio associated with the passed in #SpiceSession.
 * A new #SpiceAudio instance will be created the first time this
 * function is called for a certain #SpiceSession.
 *
 * Note that this function returns a weak reference, which should not be used
 * after the #SpiceSession itself has been unref-ed by the caller.
 *
 * Returns: (transfer none): a weak reference to a #SpiceAudio
 * instance or %NULL if failed.
 **/
SpiceAudio *spice_audio_get(SpiceSession *session, GMainContext *context)
{
    static GMutex mutex;
    SpiceAudio *self;

    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);

    g_mutex_lock(&mutex);
    self = session->priv->audio_manager;
    if (self == NULL) {
        self = spice_audio_new_priv(session, context, NULL);
        session->priv->audio_manager = self;
    }
    g_mutex_unlock(&mutex);

    return self;
}

/**
 * spice_usb_device_manager_get:
 * @session: #SpiceSession for which to get the #SpiceUsbDeviceManager
 * @err: (allow-none): a return location for #GError, or %NULL.
 *
 * Gets the #SpiceUsbDeviceManager associated with the passed in #SpiceSession.
 * A new #SpiceUsbDeviceManager instance will be created the first time this
 * function is called for a certain #SpiceSession.
 *
 * Note that this function returns a weak reference, which should not be used
 * after the #SpiceSession itself has been unref-ed by the caller.
 *
 * Returns: (transfer none): a weak reference to the #SpiceUsbDeviceManager associated with the passed in #SpiceSession
 */
SpiceUsbDeviceManager *spice_usb_device_manager_get(SpiceSession *session,
                                                    GError **err)
{
    SpiceUsbDeviceManager *self;
    static GMutex mutex;

    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
    g_return_val_if_fail(err == NULL || *err == NULL, NULL);

    g_mutex_lock(&mutex);
    self = session->priv->usb_manager;
    if (self == NULL) {
        self = g_initable_new(SPICE_TYPE_USB_DEVICE_MANAGER, NULL, err,
                              "session", session, NULL);
        session->priv->usb_manager = self;
    }
    g_mutex_unlock(&mutex);

    return self;
}

G_GNUC_INTERNAL
gboolean spice_session_get_audio_enabled(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    return session->priv->audio;
}

G_GNUC_INTERNAL
gboolean spice_session_get_usbredir_enabled(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    return session->priv->usbredir;
}

G_GNUC_INTERNAL
gboolean spice_session_get_smartcard_enabled(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    return session->priv->smartcard;
}

G_GNUC_INTERNAL
PhodavServer* spice_session_get_webdav_server(SpiceSession *session)
{
    SpiceSessionPrivate *priv;

    g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
    priv = session->priv;

#ifdef USE_PHODAV
    static GMutex mutex;

    const gchar *shared_dir = spice_session_get_shared_dir(session);
    /* with HAVE_PHODAV_VIRTUAL, PhodavServer must be created even if shared_dir is NULL */
#ifndef HAVE_PHODAV_VIRTUAL
    if (shared_dir == NULL) {
        SPICE_DEBUG("No shared dir set, not creating webdav server");
        return NULL;
    }
#endif

    g_mutex_lock(&mutex);

    if (priv->webdav == NULL) {
#ifdef HAVE_PHODAV_VIRTUAL
        PhodavVirtualDir *root = phodav_virtual_dir_new_root();
        priv->webdav = phodav_server_new_for_root_file(G_FILE(root));

        phodav_virtual_dir_root_set_real(root, shared_dir);

        g_object_unref(phodav_virtual_dir_new_dir(root, SPICE_WEBDAV_CLIPBOARD_FOLDER_PATH, NULL));
        g_object_unref(root);
#else
        priv->webdav = phodav_server_new(shared_dir);
#endif

        g_object_bind_property(session,  "share-dir-ro",
                               priv->webdav, "read-only",
                               G_BINDING_SYNC_CREATE|G_BINDING_BIDIRECTIONAL);

        /* with HAVE_PHODAV_VIRTUAL, the update is done in spice_session_set_shared_dir() */
#ifndef HAVE_PHODAV_VIRTUAL
        g_object_bind_property(session,  "shared-dir",
                               priv->webdav, "root",
                               G_BINDING_SYNC_CREATE|G_BINDING_BIDIRECTIONAL);
#endif
    }

    g_mutex_unlock(&mutex);
#endif

    return priv->webdav;
}

/**
 * spice_session_is_for_migration:
 * @session: a Spice session
 *
 * During seamless migration, channels may be created to establish a
 * connection with the target, but they are temporary and should only
 * handle migration steps. In order to avoid other interactions with
 * the client, channels should check this value.
 *
 * Returns: %TRUE if the session is a copy created during migration
 * Since: 0.27
 **/
gboolean spice_session_is_for_migration(SpiceSession *session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);

    return session->priv->for_migration;
}

G_GNUC_INTERNAL
gboolean spice_session_set_migration_session(SpiceSession *session, SpiceSession *mig_session)
{
    g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
    g_return_val_if_fail(SPICE_IS_SESSION(mig_session), FALSE);
    g_return_val_if_fail(session->priv->migration == NULL, FALSE);

    session->priv->migration = mig_session;

    return TRUE;
}
