[Spice-devel] [spice-gtk v2] Implements set_keyboard_lock_modifiers for Windows
Marc-André Lureau
marcandre.lureau at gmail.com
Wed Jun 8 17:22:19 UTC 2016
Hi
On Wed, Jun 8, 2016 at 4:26 PM, Frediano Ziglio <fziglio at redhat.com> wrote:
> Signed-off-by: Frediano Ziglio <fziglio at redhat.com>
> ---
> src/keyboard-modifiers.c | 445 +++++++++++++++++++++++++++++++++++++++++++++++
> src/keyboard-modifiers.h | 5 +
> tests/Makefile.am | 8 +
> tests/keyboard-test.c | 98 +++++++++++
> 4 files changed, 556 insertions(+)
> create mode 100644 tests/keyboard-test.c
>
> Changes from v1:
> - move test functions to .c file;
> - update code style.
>
> diff --git a/src/keyboard-modifiers.c b/src/keyboard-modifiers.c
> index dd13fad..23c55f3 100644
> --- a/src/keyboard-modifiers.c
> +++ b/src/keyboard-modifiers.c
> @@ -17,6 +17,8 @@
> */
> #include "config.h"
>
> +#include <glib.h>
> +
> #ifdef HAVE_X11_XKBLIB_H
> #include <X11/XKBlib.h>
> #include <gdk/gdkx.h>
> @@ -158,6 +160,449 @@ void set_keyboard_lock_modifiers(guint32 modifiers)
> set_keyboard_led(x_display, SCROLL_LOCK_LED, !!(modifiers & SPICE_INPUTS_SCROLL_LOCK));
> }
>
> +#elif defined(G_OS_WIN32)
> +
> +/* Some definitions from kbd.h to define internal layout file structures */
> +/* Note that pointer in Wow64 are 64 bit despite program bits */
> +#define KBDSPECIAL (USHORT)0x0400
> +
> +/* type of NLS function key */
> +#define KBDNLS_TYPE_NULL 0
> +#define KBDNLS_TYPE_NORMAL 1
> +#define KBDNLS_TYPE_TOGGLE 2
> +
> +/* action to perform on a specific combination (only needed) */
> +#define KBDNLS_NULL 0 /* Invalid function */
> +#define KBDNLS_SEND_BASE_VK 2 /* Send Base VK_xxx */
> +#define KBDNLS_SEND_PARAM_VK 3 /* Send Parameter VK_xxx */
> +
> +typedef struct {
> + BYTE NLSFEProcIndex;
> + ULONG NLSFEProcParam;
> +} VK_FPARAM;
> +
> +typedef struct {
> + BYTE Vk;
> + BYTE NLSFEProcType;
> + BYTE NLSFEProcCurrent;
> + BYTE NLSFEProcSwitch; /* 8 bits */
> + VK_FPARAM NLSFEProc[8];
> + VK_FPARAM NLSFEProcAlt[8];
> +} VK_F;
> +
> +typedef struct {
> + USHORT OEMIdentifier;
> + USHORT LayoutInformation;
> + UINT NumOfVkToF;
> + VK_F *pVkToF;
> + void *dummy; /* used to check size */
> +} KBDNLSTABLES;
> +
> +typedef void *WINAPI KbdLayerDescriptor_t(void);
> +typedef BOOL WINAPI KbdLayerRealDllFile_t(HKL hkl, WCHAR *realDllName, LPVOID pClientKbdType, LPVOID reserve1 , LPVOID reserve2);
> +typedef KBDNLSTABLES *WINAPI KbdNlsLayerDescriptor_t(void);
> +
> +/* where all keyboard layouts information are in the registry */
> +#define LAYOUT_REGKEY "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts"
> +
> +static LONG reg_read_str(HKEY key, LPCWSTR name, WCHAR *value, size_t len);
> +static HMODULE load_keyboard_layout(const WCHAR *dll_name);
> +
> +#define test_debug(fmt, ...) do { \
> + if (0) printf(fmt "\n", ## __VA_ARGS__); \
> +} while(0)
> +
Please use SPICE_DEBUG instead
> +/**
We should't use gtk-doc style for non-exported functions, that could
easily confuse the tool.
> + * Read a string from registry
> + * @key registry key to read from
> + * @name name of the value to read
> + * @value buffer where to store results
> + * @size size of value buffer in bytes (not value elements!)
> + * @returns system error code or ERROR_SUCCESS
> + */
> +static LONG reg_read_str(HKEY key, LPCWSTR name, WCHAR *value, size_t len)
> +{
> + DWORD size = len-sizeof(*value), type;
> + LONG err = RegQueryValueExW(key, name, 0, &type, (void *) value, &size);
> + if (err != ERROR_SUCCESS)
> + return err;
> +
> + if (type != REG_SZ)
> + return ERROR_INVALID_DATA;
> +
> + /* assure terminated */
> + value[size/sizeof(*value)] = 0;
> + return ERROR_SUCCESS;
> +}
> +
> +/**
> + * Load a keyboard layout file given the file name
> + * @dll_name dll name (should be just file name without paths)
> + */
> +static HMODULE load_keyboard_layout(const WCHAR *dll_name)
> +{
> + WCHAR fn[MAX_PATH+256];
> +
> +#ifdef _WIN64
> + GetSystemDirectoryW(fn, MAX_PATH);
> +#else
> + typedef UINT WINAPI GetSystemWow64DirectoryW_t(LPWSTR str, UINT size);
> + GetSystemWow64DirectoryW_t *pGetSystemWow64DirectoryW =
> + (GetSystemWow64DirectoryW_t*)GetProcAddress(GetModuleHandle("kernel32"), "GetSystemWow64DirectoryW");
> + if (!pGetSystemWow64DirectoryW || pGetSystemWow64DirectoryW(fn, MAX_PATH) == 0)
> + GetSystemDirectoryW(fn, MAX_PATH);
> +#endif
> + wcscat(fn, L"\\");
> + wcscat(fn, dll_name);
> +
> + test_debug("loading file %S", fn);
> + return LoadLibraryW(fn);
> +}
> +
> +/**
same, and others below
> + * Check if current process is running in Wow64 mode
> + * (32 bit on a 64 system)
> + */
> +#ifndef _WIN64
> +static BOOL is_wow64(void)
> +{
> + BOOL bIsWow64 = FALSE;
> +
> + typedef BOOL WINAPI IsWow64Process_t(HANDLE, PBOOL);
> + IsWow64Process_t *pIsWow64Process =
> + (IsWow64Process_t *) GetProcAddress(GetModuleHandle("kernel32"), "IsWow64Process");
> +
> + if (pIsWow64Process)
> + pIsWow64Process(GetCurrentProcess(), &bIsWow64);
> +
> + return bIsWow64;
> +}
> +#else
> +static inline BOOL is_wow64(void)
> +{
> + return FALSE;
> +}
> +#endif
> +
> +/**
> + * Check if OS is 64 bit
> + */
> +static BOOL os_is_64bit(void)
> +{
> +#ifdef _WIN64
> + return TRUE;
> +#else
> + return is_wow64() != FALSE;
> +#endif
> +}
> +
This could eventually go in a seperate patch in spice-util.c/priv.h
> +
> +/* keyboard status
> + * caps lock VK/SC
> + * combination (ctrl+alt+shift / none)
> + * ctrl/alt/shift keys VK/SC (2 for each) ?
> + */
> +static WORD vsc_capital = 58;
> +static int specific_modifiers = -1;
> +
> +/**
> + * Extract information from keyboard.
> + * Currently scancode of Caps Lock is searched and
> + * possible modifiers needed to have that Caps Lock.
> + * @layout layout to get information
> + */
> +void keyboard_cache(HKL layout)
> +{
> + WCHAR buf[256];
> + HKEY key;
> + LONG err;
> +
> + KbdLayerDescriptor_t *get_desc;
> +
> + const BYTE *kbd_table;
> + int num_keys, i;
> + const USHORT *keys;
> + const KBDNLSTABLES *nls_table;
> + const VK_F *vkf, *vkf_end;
> +
> + /* set default output, usually work with lot of keyboard layout
> + these values will be used in case of errors */
> + vsc_capital = MapVirtualKey(VK_CAPITAL, MAPVK_VK_TO_VSC);
> + specific_modifiers = -1;
> +
> + /* get keyboard dll name from registry */
> + swprintf(buf, 256, TEXT(LAYOUT_REGKEY) L"\\%08X", (unsigned)(DWORD_PTR)layout);
> + err = RegOpenKeyExW(HKEY_LOCAL_MACHINE, buf, 0, KEY_READ, &key);
> + if (err != ERROR_SUCCESS) {
> + g_critical("failed getting keyboard layout registry key");
> + return;
> + }
> + err = reg_read_str(key, L"Layout File", buf, sizeof(buf));
> + RegCloseKey(key);
> + if (err != ERROR_SUCCESS) {
> + g_critical("failed getting keyboard layout file name");
> + return;
> + }
> +
> + /* load keyboard layout file */
> + HMODULE dll = load_keyboard_layout(buf);
> + if (!dll) {
> + g_critical("error loading keyboard layout for %08x", (unsigned)(DWORD_PTR)layout);
> + return;
> + }
> +
> + /* see if we need to get another dll */
> + KbdLayerRealDllFile_t *dll_file = (KbdLayerRealDllFile_t *) GetProcAddress(dll, "KbdLayerRealDllFile");
> + if (dll_file) {
> + /* load the other file */
> + if (dll_file(layout, buf, NULL, NULL, NULL)) {
> + test_debug("dll redirected to %S %u", buf, (unsigned) wcslen(buf));
> + HMODULE new_dll = load_keyboard_layout(buf);
> + if (new_dll) {
> + test_debug("unloading stub");
> + FreeLibrary(dll);
> + dll = new_dll;
> + }
> + }
> + }
> +
> + /* check if there are NLS function (in this case we must parse tables) */
> + KbdNlsLayerDescriptor_t *get_nls_desc = (KbdNlsLayerDescriptor_t *) GetProcAddress(dll, "KbdNlsLayerDescriptor");
> + if (!get_nls_desc)
> + goto cleanup;
> +
> + /* get main keyboard table */
> + get_desc = (KbdLayerDescriptor_t *) GetProcAddress(dll, "KbdLayerDescriptor");
> + if (!get_desc) {
> + g_critical("keyboard dll layout has no descriptor");
> + goto cleanup;
> + }
> + kbd_table = (BYTE *) get_desc();
> +
> + /* check table (for Win32 see format 32 or 64) */
> + if (os_is_64bit()) {
> + test_debug("64 bit");
> + if (IsBadReadPtr(kbd_table, 12*8)) {
> + g_critical("wrong table address");
> + goto cleanup;
> + }
> + keys = *((USHORT **) &kbd_table[6*8]);
> + num_keys = kbd_table[7*8];
> + } else {
> + test_debug("32 bit");
> + if (IsBadReadPtr(kbd_table, 13*4)) {
> + g_critical("wrong table address");
> + goto cleanup;
> + }
> + keys = *((USHORT **) &kbd_table[6*4]);
> + num_keys = kbd_table[7*4];
> + }
> +
> + /* scan VKs for a VK_CAPITAL not special */
> + if (IsBadReadPtr(keys, num_keys*sizeof(*keys))) {
> + g_critical("wrong VKs table");
> + goto cleanup;
> + }
> + for (i = 0; i < num_keys; ++i) {
> + /* ... return if found */
> + if ((keys[i] & KBDSPECIAL) == 0 && (keys[i] & 0xFF) == VK_CAPITAL) {
> + vsc_capital = i;
> + specific_modifiers = -1;
> + goto cleanup;
> + }
> + }
> +
> + /* scan NLS table for VK_CAPITALs */
> + nls_table = get_nls_desc();
> + if (IsBadReadPtr(keys, sizeof(*nls_table))) {
> + g_critical("wrong NLS table");
> + goto cleanup;
> + }
> + vkf = nls_table->pVkToF;
> + if (IsBadReadPtr(vkf, sizeof(*vkf) * nls_table->NumOfVkToF)) {
> + g_critical("wrong function table");
> + goto cleanup;
> + }
> + test_debug("layout has %u NLS key", (unsigned) nls_table->NumOfVkToF);
> + vkf_end = vkf + nls_table->NumOfVkToF;
> + for (; vkf < vkf_end; ++vkf) {
> + unsigned mask = 0;
> + const VK_FPARAM *params = vkf->NLSFEProc;
> +
> + /* scan all functions searching for VK_CAPITAL
> + check both part, normal and with alternate (if present) */
> + for (i = 0; i < 16; ++i) {
> + if ((vkf->Vk == VK_CAPITAL && params[i].NLSFEProcIndex == KBDNLS_SEND_BASE_VK)
> + || (params[i].NLSFEProcIndex == KBDNLS_SEND_PARAM_VK && params[i].NLSFEProcParam == VK_CAPITAL))
> + mask |= 1<<i;
> + }
> + /* no VK_CAPITAL found */
> + if (!mask)
> + continue;
> + test_debug("found mask %x at vk %x", mask, vkf->Vk);
> + /* see if there are a common key between the two tables for
> + each special key */
> + unsigned common = (mask >> 8) & mask;
> + if (common)
> + mask = common;
> + for (i = 0; i < 16; ++i)
> + if (mask & (1<<i)) {
> + specific_modifiers = i & 7;
> + break;
> + }
> + /* get back base VK */
> + for (i = 0; i < num_keys; ++i) {
> + /* ... return if found */
> + test_debug("keys %d = %x vk %x", i, keys[i], vkf->Vk);
> + if ((keys[i] & KBDSPECIAL) != 0 && (keys[i] & 0xFF) == vkf->Vk) {
> + vsc_capital = i;
> + goto cleanup;
> + }
> + }
> + /* this is unexpected, there should be a key matching the NLS table */
> + g_critical("NLS key not found in normal table");
> + }
> + specific_modifiers = -1;
> +
> +cleanup:
> + test_debug("unloading dll");
> + FreeLibrary(dll);
> +}
> +
> +/**
> + * Add input keys in order to make the special key (shift/control/alt)
> + * state the same as wanted one.
> + * @vk virtual key of the key
> + * @curr_state current state (!=0 is key down)
> + * @wanted_state wanted state (!=0 is key down)
> + * @begin_inputs beginning of already present input keys
> + * @end_inputs end of already present input keys
> + */
> +static void adjust_special(WORD vk, int curr_state, int wanted_state,
> + INPUT **begin_inputs, INPUT **end_inputs)
> +{
> + KEYBDINPUT *ki;
> +
> + curr_state &= 0x80;
> + if (!!wanted_state == !!curr_state)
> + return;
> +
> + /* if there are not the spcific key no need to handle */
> + UINT vsc = MapVirtualKey(vk, MAPVK_VK_TO_VSC);
> + if (!vsc)
> + return;
> +
> + /* make sure modifier key is in the right state before pressing
> + main key */
> + --(*begin_inputs);
> + ki = &(*begin_inputs)->ki;
> + ki->wVk = vk;
> + ki->wScan = vsc;
> + ki->dwFlags = wanted_state ? 0 : KEYEVENTF_KEYUP;
> +
> + /* make sure key state is restored at the end */
> + ki = &(*end_inputs)->ki;
> + ki->wVk = vk;
> + ki->wScan = vsc;
> + ki->dwFlags = wanted_state ? KEYEVENTF_KEYUP : 0;
> + ++(*end_inputs);
> +}
> +
> +/**
> + * Add a key pression and a release to inputs events
> + */
> +static gboolean add_press_release(INPUT *inputs, WORD vk, WORD vsc)
> +{
> + KEYBDINPUT *ki;
> +
> + if (!vsc)
> + return FALSE;
> +
> + ki = &inputs[0].ki;
> + ki->wVk = vk;
> + ki->wScan = vsc;
> + ki = &inputs[1].ki;
> + ki->wVk = vk;
> + ki->wScan = vsc;
> + ki->dwFlags = KEYEVENTF_KEYUP;
> +
> + return TRUE;
> +}
> +
> +void set_keyboard_lock_modifiers(guint32 modifiers)
We use G_GNUC_INTERNAL when possible (although it should be placed at
declaration for odd compilers etc, we do it on definition)
> +{
> + static HKL cached_layout = 0;
> +
> + /* get keyboard layout */
> + HKL curr_layout = GetKeyboardLayout(0);
> +
> + /* same as before, use old informations.. */
> + if (curr_layout != cached_layout) {
> + /* .. otherwise cache new keyboard layout */
> + keyboard_cache(curr_layout);
> + cached_layout = curr_layout;
> + }
> +
> + BYTE key_states[256];
> + GetKeyboardState(key_states);
> +
> + /* compute sequence to press
> + * as documented in SetKeyboardState we must press
> + * the sequence that cause the state to change
> + * to modify the global state */
> + int i;
> + INPUT inputs[6*2+2+4], *begin_inputs, *end_inputs;
> + memset(inputs, 0, sizeof(inputs));
> + for (i = 0; i < G_N_ELEMENTS(inputs); ++i) {
> + inputs[i].type = INPUT_KEYBOARD;
> + inputs[i].ki.dwExtraInfo = 0x12345678;
> + }
> +
> + /* start pointers, make sure we have enough space
> + before and after to insert shift/control/alt keys */
> + begin_inputs = end_inputs = inputs + 6;
> +
> + /* we surely must press the caps lock key */
> + if ((!!(modifiers & SPICE_INPUTS_CAPS_LOCK) != !!(key_states[VK_CAPITAL] & 0x80))
> + && add_press_release(end_inputs, VK_CAPITAL, vsc_capital)) {
> + end_inputs += 2;
> +
> + /* unfortunately the key expect a specific combination
> + of shift/control/alt, make sure we have that state */
> + if (specific_modifiers >= 0) {
> +
> +#define adjust_special(vk, mask) \
> + adjust_special((vk), key_states[vk], specific_modifiers & (mask), &begin_inputs, &end_inputs)
> +
> + adjust_special(VK_LSHIFT, 1);
> + adjust_special(VK_RSHIFT, 1);
> + adjust_special(VK_LCONTROL, 2);
> + adjust_special(VK_RCONTROL, 2);
> + adjust_special(VK_LMENU, 4);
> + adjust_special(VK_RMENU, 4);
> + }
> + }
> +
> + /* sync NUMLOCK */
> + if ((!!(modifiers & SPICE_INPUTS_NUM_LOCK) != !!(key_states[VK_NUMLOCK] & 0x80))
> + && add_press_release(end_inputs, VK_NUMLOCK, MapVirtualKey(VK_NUMLOCK, MAPVK_VK_TO_VSC))) {
> + end_inputs += 2;
> + }
> +
> + /* sync SCROLLLOCK */
> + if ((!!(modifiers & SPICE_INPUTS_SCROLL_LOCK) != !!(key_states[VK_SCROLL] & 0x80))
> + && add_press_release(end_inputs, VK_SCROLL, MapVirtualKey(VK_SCROLL, MAPVK_VK_TO_VSC))) {
> + end_inputs += 2;
> + }
> +
> + /* press the sequence */
> + if (end_inputs > begin_inputs) {
> + BlockInput(TRUE);
> + SendInput(end_inputs - begin_inputs, begin_inputs, sizeof(inputs[0]));
> + BlockInput(FALSE);
> + }
> +}
> +
> #else
>
> void set_keyboard_lock_modifiers(guint32 modifiers)
> diff --git a/src/keyboard-modifiers.h b/src/keyboard-modifiers.h
> index 016be84..6dad37e 100644
> --- a/src/keyboard-modifiers.h
> +++ b/src/keyboard-modifiers.h
> @@ -30,6 +30,11 @@ G_BEGIN_DECLS
> guint32 get_keyboard_lock_modifiers(void);
> void set_keyboard_lock_modifiers(guint32 modifiers);
>
> +#ifdef G_OS_WIN32
> +#include <windows.h>
> +void keyboard_cache(HKL layout);
> +#endif
> +
> G_END_DECLS
>
> #endif /* KEYBOARD_MODIFIERS_H */
> diff --git a/tests/Makefile.am b/tests/Makefile.am
> index 1a8b768..1d2006b 100644
> --- a/tests/Makefile.am
> +++ b/tests/Makefile.am
> @@ -16,11 +16,18 @@ TESTS += usb-acl-helper
> noinst_PROGRAMS += mock-acl-helper
> endif
>
> +if OS_WIN32
> +TESTS += keyboard-test
> +keyboard_test_SOURCES = \
> + keyboard-test.c \
> + $(NULL)
> +endif
> noinst_PROGRAMS += $(TESTS)
>
> AM_CPPFLAGS = \
> $(COMMON_CFLAGS) \
> $(GIO_CFLAGS) \
> + $(GTK_CFLAGS) \
> $(SMARTCARD_CFLAGS) \
> -I$(top_srcdir)/src \
> -I$(top_builddir)/src \
> @@ -30,6 +37,7 @@ AM_CPPFLAGS = \
> AM_LDFLAGS = $(GIO_LIBS) -static
>
> LDADD = \
> + $(top_builddir)/src/libspice-client-gtk-3.0.la \
> $(top_builddir)/src/libspice-client-glib-2.0.la \
> $(NULL)
>
> diff --git a/tests/keyboard-test.c b/tests/keyboard-test.c
> new file mode 100644
> index 0000000..377156e
> --- /dev/null
> +++ b/tests/keyboard-test.c
> @@ -0,0 +1,98 @@
> +#include "config.h"
> +
> +#include <glib.h>
> +
> +#include <windows.h>
> +#include <stdio.h>
> +
> +#include "spice-client-gtk.h"
> +
> +#define SPICE_COMPILATION
> +#include "keyboard-modifiers.h"
> +
> +#define LAYOUT_REGKEY "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts"
> +
> +static void keyboard_check_single(HKEY key, const char *name);
> +
> +/* must be tested on
> + * - WinXP 32
> + * - Win7 32/64 app
> + * - Win10 32/64 app
> + */
> +static void keyboard_modifiers_test(void)
> +{
> + setbuf(stdout, NULL);
> + setbuf(stderr, NULL);
> +
> + /* scan all keyboards
> + * HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\<00030402>
> + * check them all */
> + HKEY key;
> + LONG err;
> + err = RegOpenKeyEx(HKEY_LOCAL_MACHINE, LAYOUT_REGKEY, 0, KEY_READ, &key);
> + if (err != ERROR_SUCCESS) {
> + g_error("RegOpenKeyEx error %ld", err);
> + return;
> + }
> +
> + unsigned i;
> + for (i = 0; ; ++i) {
> + char name[64];
> + err = RegEnumKey(key, i, name, G_N_ELEMENTS(name));
> + if (err == ERROR_NO_MORE_ITEMS)
> + break;
> + if (err != ERROR_SUCCESS) {
> + g_error("RegEnumKey error %ld", err);
> + break;
> + }
> + keyboard_check_single(key, name);
> + }
> +
> + RegCloseKey(key);
> + /* check for multiple keyboards
> + *
> + * KbdLayerDescriptor - no parameter, returns a table
> + * KbdLayerMultiDescriptor
> + * pass a pointer, structure like, output
> + * struct Xxx {
> + * uint32_t num_layout_valid;
> + * struct {
> + * WCHAR dll_name[32];
> + * uint32_t unknown1;
> + * uint32_t unknown2;
> + * } layers[8];
> + * }
> + * KbdLayerRealDllFile
> + * BOOL KbdLayerRealDllFile(HKL hkl, WCHAR *realDllName, PCLIENTKEYBOARDTYPE pClientKbdType, LPVOID reserve)
> + * returns TRUE if we need to load another file (this is just a stub)
> + * realDllName returned keyboard name
> + * pClientKbdType used for terminal server, NULL for physical one
> + * reserve NULL
> + * KbdLayerRealDllFileNT4 - obsolete
> + * KbdNlsLayerDescriptor - no parameter, returns NLS table, if not no need to parse but MapVirtualKey works
> + */
> +}
> +
> +static void keyboard_check_single(HKEY key, const char *name)
> +{
> + char *end = NULL;
> + errno = 0;
> + unsigned long num = strtoul(name, &end, 16);
> + if (errno || *end) {
> + g_error("wrong value %s", name);
> + return;
> + }
> +
> + printf("trying keyboard %s\n", name);
> + keyboard_cache((HKL) (DWORD_PTR) num);
> + printf("----\n");
> +}
> +
> +int main(void)
> +{
> + /* make g_critical abort */
> + g_log_set_fatal_mask(G_LOG_DOMAIN, G_LOG_FATAL_MASK|G_LOG_LEVEL_ERROR|G_LOG_LEVEL_CRITICAL);
> +
> + keyboard_modifiers_test();
> + return 0;
> +}
> --
> 2.7.4
>
> _______________________________________________
> Spice-devel mailing list
> Spice-devel at lists.freedesktop.org
> https://lists.freedesktop.org/mailman/listinfo/spice-devel
--
Marc-André Lureau
More information about the Spice-devel
mailing list