mirror of
https://dev.lirent.ru/Vatrog/vm-control-io.git
synced 2026-06-18 02:36:37 +03:00
vmctl: input-control library over QMP and Linux uinput
QMP client and driver, Linux uinput driver, keymap, open/power helpers, public header, and CMake build.
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
.*/
|
||||||
|
cmake-*/
|
||||||
|
compile*
|
||||||
|
docs/plans/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(vmctl C)
|
||||||
|
set(CMAKE_C_STANDARD 17)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_C_EXTENSIONS ON) # POSIX sockets + linux/uinput.h need gnu extensions
|
||||||
|
option(VMCTL_LTO "Enable LTO" OFF)
|
||||||
|
add_library(vmctl STATIC
|
||||||
|
src/vmctl/qmp.c
|
||||||
|
src/vmctl/open.c
|
||||||
|
src/vmctl/keymap.c
|
||||||
|
src/vmctl/power.c
|
||||||
|
src/vmctl/qmp_driver.c
|
||||||
|
src/vmctl/linux/uinput_driver.c)
|
||||||
|
target_include_directories(vmctl
|
||||||
|
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/vmctl/include)
|
||||||
|
target_compile_options(vmctl PRIVATE -O2 -Wall -Wextra)
|
||||||
|
if(VMCTL_LTO)
|
||||||
|
target_compile_options(vmctl PRIVATE -flto)
|
||||||
|
target_link_options(vmctl PRIVATE -flto)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#ifndef VMCTL_H
|
||||||
|
#define VMCTL_H
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* vmctl.h — public API for a QEMU VM Input layer (actuator): input injection +
|
||||||
|
* power/lifecycle actuation. One handle; the input driver is selected
|
||||||
|
* declaratively through vmctl_config. OS-agnostic surface. */
|
||||||
|
|
||||||
|
typedef struct vmctl vmctl_t; /* opaque handle */
|
||||||
|
|
||||||
|
/* ===== Input drivers + open ===== */
|
||||||
|
typedef enum {
|
||||||
|
VMCTL_DRIVER_QMP, /* QMP input-send-event (no guest driver required) */
|
||||||
|
VMCTL_DRIVER_UINPUT /* host uinput source; optional passthrough into guest */
|
||||||
|
/* via QEMU virtio-input-host-pci (Linux). uinput != virtio. */
|
||||||
|
} vmctl_driver;
|
||||||
|
|
||||||
|
#define VMCTL_PTR_ABS 1 /* uinput: absolute tablet */
|
||||||
|
#define VMCTL_PTR_REL 2 /* uinput: relative mouse */
|
||||||
|
#define VMCTL_PTR_BOTH 3 /* uinput: two devices A=abs B=rel */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
vmctl_driver driver;
|
||||||
|
const char* qmp_path; /* QMP unix socket; required for QMP, optional (passthrough) for UINPUT */
|
||||||
|
const char* input_bus; /* virtio-input-host-pci bus "pci.0" for passthrough; "" = none */
|
||||||
|
int ptr_mode; /* UINPUT VMCTL_PTR_*; 0 for QMP */
|
||||||
|
} vmctl_config;
|
||||||
|
|
||||||
|
vmctl_t* vmctl_open (const vmctl_config* cfg); /* NULL on error */
|
||||||
|
void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||||
|
|
||||||
|
/* ===== Input constants ===== */
|
||||||
|
#define VMCTL_ABS_MAX 32767 /* abs coordinates 0..VMCTL_ABS_MAX */
|
||||||
|
#define VMCTL_AXIS_X 0
|
||||||
|
#define VMCTL_AXIS_Y 1
|
||||||
|
#define VMCTL_SCROLL_V 0 /* vertical */
|
||||||
|
#define VMCTL_SCROLL_H 1 /* horizontal */
|
||||||
|
#define VMCTL_BTN_LEFT 0
|
||||||
|
#define VMCTL_BTN_RIGHT 1
|
||||||
|
#define VMCTL_BTN_MIDDLE 2
|
||||||
|
#define VMCTL_BTN_SIDE 3
|
||||||
|
#define VMCTL_BTN_EXTRA 4
|
||||||
|
#define VMCTL_BTN_FORWARD 5
|
||||||
|
#define VMCTL_BTN_BACK 6
|
||||||
|
#define VMCTL_BTN_TASK 7
|
||||||
|
|
||||||
|
/* ===== Event batch (value-type, stack; build ONLY via builders — ev[] is not API) ===== */
|
||||||
|
#define VMCTL_BATCH_MAX 64
|
||||||
|
typedef struct {
|
||||||
|
int kind; /* internal event-kind code; set by builders */
|
||||||
|
int code; /* axis / button / evdev-code (per kind) */
|
||||||
|
int value; /* abs-value / rel-delta / down(0|1) */
|
||||||
|
double scroll; /* scroll magnitude (scroll only) */
|
||||||
|
} vmctl_event;
|
||||||
|
typedef struct { vmctl_event ev[VMCTL_BATCH_MAX]; int count; } vmctl_batch;
|
||||||
|
|
||||||
|
void vmctl_batch_init (vmctl_batch* b);
|
||||||
|
void vmctl_batch_abs (vmctl_batch* b, int axis, int value);
|
||||||
|
void vmctl_batch_rel (vmctl_batch* b, int axis, int delta);
|
||||||
|
void vmctl_batch_btn (vmctl_batch* b, int btn, int down);
|
||||||
|
void vmctl_batch_key (vmctl_batch* b, int evdev_code, int down);
|
||||||
|
void vmctl_batch_scroll(vmctl_batch* b, int axis, double value);
|
||||||
|
int vmctl_batch_send (vmctl_t* v, vmctl_batch* b); /* one round-trip; 0=ok, -1=err */
|
||||||
|
|
||||||
|
/* ===== Single events (wrappers over a 1-event batch) ===== */
|
||||||
|
int vmctl_abs (vmctl_t* v, int axis, int value); /* 0..VMCTL_ABS_MAX */
|
||||||
|
int vmctl_rel (vmctl_t* v, int axis, int delta);
|
||||||
|
int vmctl_btn (vmctl_t* v, int btn, int down); /* VMCTL_BTN_* */
|
||||||
|
int vmctl_key (vmctl_t* v, int evdev_code, int down); /* Linux KEY_* */
|
||||||
|
int vmctl_scroll(vmctl_t* v, int axis, double value); /* VMCTL_SCROLL_* */
|
||||||
|
|
||||||
|
/* ===== Power/lifecycle actuation (requires a QMP connection; -1 if there is none) ===== */
|
||||||
|
int vmctl_powerdown(vmctl_t* v); /* system_powerdown (ACPI soft-off) */
|
||||||
|
int vmctl_reset (vmctl_t* v); /* system_reset */
|
||||||
|
int vmctl_wakeup (vmctl_t* v); /* system_wakeup (from S3/S4) */
|
||||||
|
int vmctl_pause (vmctl_t* v); /* stop */
|
||||||
|
int vmctl_resume (vmctl_t* v); /* cont */
|
||||||
|
|
||||||
|
/* Последовательность/контекст передачи — signaling; тайминги и решения — control;
|
||||||
|
* чтение состояния VM — сенсоры. Здесь, в Input-слое, только атомарная актуация. */
|
||||||
|
|
||||||
|
#endif /* VMCTL_H */
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef VMCTL_DRIVER_H
|
||||||
|
#define VMCTL_DRIVER_H
|
||||||
|
#include "vmctl.h"
|
||||||
|
#include "qmp.h"
|
||||||
|
|
||||||
|
/* driver.h — input-driver vtable, the concrete vmctl handle, and the shared
|
||||||
|
* event-kind enum. The event kind is the SINGLE source of truth that every
|
||||||
|
* driver switches on (never on magic numbers). */
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
VMCTL_EV_ABS, VMCTL_EV_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
|
||||||
|
} vmctl_ev_kind;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int (*send)(vmctl_t* v, const vmctl_batch* b); /* deliver an input batch */
|
||||||
|
void (*close)(vmctl_t* v); /* release driver resources */
|
||||||
|
} vmctl_driver_ops;
|
||||||
|
|
||||||
|
struct vmctl {
|
||||||
|
vmctl_driver_ops ops;
|
||||||
|
vmctl_driver driver;
|
||||||
|
qmp_conn* qmp; /* control channel; NULL if none */
|
||||||
|
int ui_fd_a; /* uinput driver: device A; -1 for QMP */
|
||||||
|
int ui_fd_b; /* uinput driver: device B (BOTH); -1 */
|
||||||
|
int ptr_mode; /* uinput driver: VMCTL_PTR_*; 0 for QMP */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* driver factories (called from open.c per cfg->driver) */
|
||||||
|
vmctl_t* vmctl_open_qmp_driver (const vmctl_config* cfg);
|
||||||
|
vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg);
|
||||||
|
|
||||||
|
#endif /* VMCTL_DRIVER_H */
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef VMCTL_KEYMAP_H
|
||||||
|
#define VMCTL_KEYMAP_H
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* keymap.h — the single source of truth for keyboard keys. One descriptor maps
|
||||||
|
* a Linux evdev code to a QEMU QKeyCode name. Both the QMP and uinput drivers
|
||||||
|
* derive everything from this table. */
|
||||||
|
|
||||||
|
/* NOTE: named vmctl_keymap, not vmctl_key — the public API uses the ordinary
|
||||||
|
* identifier vmctl_key for the key-injection function (include/vmctl.h), and a
|
||||||
|
* typedef would collide with it. */
|
||||||
|
typedef struct { int evdev; const char* qcode; } vmctl_keymap;
|
||||||
|
extern const vmctl_keymap VMCTL_KEYS[]; /* sorted by evdev (for bsearch) */
|
||||||
|
extern const int VMCTL_KEYS_LEN;
|
||||||
|
|
||||||
|
const char* vmctl_evdev_to_qcode(int evdev); /* NULL if absent */
|
||||||
|
|
||||||
|
#endif /* VMCTL_KEYMAP_H */
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#ifndef VMCTL_QMP_H
|
||||||
|
#define VMCTL_QMP_H
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* qmp.h — minimal QMP client over an AF_UNIX socket: connect (with capability
|
||||||
|
* negotiation), disconnect, and synchronous command execution. */
|
||||||
|
|
||||||
|
typedef struct qmp_conn qmp_conn;
|
||||||
|
|
||||||
|
qmp_conn* qmp_connect(const char* sock_path); /* connect + qmp_capabilities; NULL on error */
|
||||||
|
void qmp_disconnect(qmp_conn* c);
|
||||||
|
int qmp_exec(qmp_conn* c, const char* cmd, char* resp, size_t cap); /* 0=return, -1=error */
|
||||||
|
|
||||||
|
#endif /* VMCTL_QMP_H */
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/* keymap.c — the single source of truth for keyboard keys. VMCTL_KEYS maps
|
||||||
|
* Linux evdev codes to QEMU QKeyCode names (sorted by evdev for bsearch);
|
||||||
|
* vmctl_evdev_to_qcode is the sole lookup, consumed by the QMP driver. */
|
||||||
|
|
||||||
|
#include "keymap.h"
|
||||||
|
|
||||||
|
#include <linux/input-event-codes.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
const vmctl_keymap VMCTL_KEYS[] = {
|
||||||
|
{ KEY_ESC, "esc" },
|
||||||
|
{ KEY_1, "1" },
|
||||||
|
{ KEY_2, "2" },
|
||||||
|
{ KEY_3, "3" },
|
||||||
|
{ KEY_4, "4" },
|
||||||
|
{ KEY_5, "5" },
|
||||||
|
{ KEY_6, "6" },
|
||||||
|
{ KEY_7, "7" },
|
||||||
|
{ KEY_8, "8" },
|
||||||
|
{ KEY_9, "9" },
|
||||||
|
{ KEY_0, "0" },
|
||||||
|
{ KEY_MINUS, "minus" },
|
||||||
|
{ KEY_EQUAL, "equal" },
|
||||||
|
{ KEY_BACKSPACE, "backspace" },
|
||||||
|
{ KEY_TAB, "tab" },
|
||||||
|
{ KEY_Q, "q" },
|
||||||
|
{ KEY_W, "w" },
|
||||||
|
{ KEY_E, "e" },
|
||||||
|
{ KEY_R, "r" },
|
||||||
|
{ KEY_T, "t" },
|
||||||
|
{ KEY_Y, "y" },
|
||||||
|
{ KEY_U, "u" },
|
||||||
|
{ KEY_I, "i" },
|
||||||
|
{ KEY_O, "o" },
|
||||||
|
{ KEY_P, "p" },
|
||||||
|
{ KEY_LEFTBRACE, "bracket_left" },
|
||||||
|
{ KEY_RIGHTBRACE, "bracket_right" },
|
||||||
|
{ KEY_ENTER, "ret" },
|
||||||
|
{ KEY_LEFTCTRL, "ctrl" },
|
||||||
|
{ KEY_A, "a" },
|
||||||
|
{ KEY_S, "s" },
|
||||||
|
{ KEY_D, "d" },
|
||||||
|
{ KEY_F, "f" },
|
||||||
|
{ KEY_G, "g" },
|
||||||
|
{ KEY_H, "h" },
|
||||||
|
{ KEY_J, "j" },
|
||||||
|
{ KEY_K, "k" },
|
||||||
|
{ KEY_L, "l" },
|
||||||
|
{ KEY_SEMICOLON, "semicolon" },
|
||||||
|
{ KEY_APOSTROPHE, "apostrophe" },
|
||||||
|
{ KEY_GRAVE, "grave_accent" },
|
||||||
|
{ KEY_LEFTSHIFT, "shift" },
|
||||||
|
{ KEY_BACKSLASH, "backslash" },
|
||||||
|
{ KEY_Z, "z" },
|
||||||
|
{ KEY_X, "x" },
|
||||||
|
{ KEY_C, "c" },
|
||||||
|
{ KEY_V, "v" },
|
||||||
|
{ KEY_B, "b" },
|
||||||
|
{ KEY_N, "n" },
|
||||||
|
{ KEY_M, "m" },
|
||||||
|
{ KEY_COMMA, "comma" },
|
||||||
|
{ KEY_DOT, "dot" },
|
||||||
|
{ KEY_SLASH, "slash" },
|
||||||
|
{ KEY_RIGHTSHIFT, "shift_r" },
|
||||||
|
{ KEY_LEFTALT, "alt" },
|
||||||
|
{ KEY_SPACE, "spc" },
|
||||||
|
{ KEY_CAPSLOCK, "caps_lock" },
|
||||||
|
{ KEY_F1, "f1" },
|
||||||
|
{ KEY_F2, "f2" },
|
||||||
|
{ KEY_F3, "f3" },
|
||||||
|
{ KEY_F4, "f4" },
|
||||||
|
{ KEY_F5, "f5" },
|
||||||
|
{ KEY_F6, "f6" },
|
||||||
|
{ KEY_F7, "f7" },
|
||||||
|
{ KEY_F8, "f8" },
|
||||||
|
{ KEY_F9, "f9" },
|
||||||
|
{ KEY_F10, "f10" },
|
||||||
|
{ KEY_NUMLOCK, "num_lock" },
|
||||||
|
{ KEY_SCROLLLOCK, "scroll_lock" },
|
||||||
|
{ KEY_102ND, "less" },
|
||||||
|
{ KEY_F11, "f11" },
|
||||||
|
{ KEY_F12, "f12" },
|
||||||
|
{ KEY_RIGHTCTRL, "ctrl_r" },
|
||||||
|
{ KEY_SYSRQ, "print" },
|
||||||
|
{ KEY_RIGHTALT, "alt_r" },
|
||||||
|
{ KEY_HOME, "home" },
|
||||||
|
{ KEY_UP, "up" },
|
||||||
|
{ KEY_PAGEUP, "pgup" },
|
||||||
|
{ KEY_LEFT, "left" },
|
||||||
|
{ KEY_RIGHT, "right" },
|
||||||
|
{ KEY_END, "end" },
|
||||||
|
{ KEY_DOWN, "down" },
|
||||||
|
{ KEY_PAGEDOWN, "pgdn" },
|
||||||
|
{ KEY_INSERT, "insert" },
|
||||||
|
{ KEY_DELETE, "delete" },
|
||||||
|
{ KEY_POWER, "power" },
|
||||||
|
{ KEY_PAUSE, "pause" },
|
||||||
|
{ KEY_LEFTMETA, "meta_l" },
|
||||||
|
{ KEY_RIGHTMETA, "meta_r" },
|
||||||
|
{ KEY_SLEEP, "sleep" },
|
||||||
|
{ KEY_WAKEUP, "wake" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const int VMCTL_KEYS_LEN = (int)(sizeof VMCTL_KEYS / sizeof VMCTL_KEYS[0]);
|
||||||
|
|
||||||
|
static int key_cmp(const void* a, const void* b) {
|
||||||
|
return ((const vmctl_keymap*)a)->evdev - ((const vmctl_keymap*)b)->evdev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* vmctl_evdev_to_qcode(int evdev) {
|
||||||
|
vmctl_keymap k = { .evdev = evdev, .qcode = NULL };
|
||||||
|
const vmctl_keymap* e = bsearch(&k, VMCTL_KEYS, (size_t)VMCTL_KEYS_LEN,
|
||||||
|
sizeof VMCTL_KEYS[0], key_cmp);
|
||||||
|
return e ? e->qcode : NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
/* uinput_driver.c — Linux uinput input driver (host source) plus optional
|
||||||
|
* passthrough into the guest. TWO distinct layers, not to be confused:
|
||||||
|
*
|
||||||
|
* (1) uinput — the host side: the library creates a /dev/input/eventN node
|
||||||
|
* and writes struct input_event into it on the hot path (uinput_driver_send).
|
||||||
|
*
|
||||||
|
* (2) virtio-input-host-pci — a QEMU device that forwards that host evdev node
|
||||||
|
* into the guest. It is an OPTIONAL setup step performed over QMP at open
|
||||||
|
* (device_add) and undone at close (device_del). It is NOT a per-event
|
||||||
|
* mechanism and lives entirely in the hotplug helpers below.
|
||||||
|
*
|
||||||
|
* uinput != virtio. Without qmp_path/input_bus the uinput device is created
|
||||||
|
* orphaned (an external layer may forward it). The driver switches on
|
||||||
|
* vmctl_ev_kind (never on magic numbers). */
|
||||||
|
|
||||||
|
#include "driver.h"
|
||||||
|
#include "keymap.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <linux/uinput.h>
|
||||||
|
#include <linux/input-event-codes.h>
|
||||||
|
|
||||||
|
/* HID identity of the synthesized device (values preserved — behaviour unchanged). */
|
||||||
|
#define HWID_BUS 0x0003
|
||||||
|
#define HWID_VENDOR 0x046D
|
||||||
|
#define HWID_PRODUCT 0xC52B
|
||||||
|
#define HWID_VERSION 0x0111
|
||||||
|
#define HWID_NAME_A "VMInput-A"
|
||||||
|
#define HWID_NAME_B "VMInput-B"
|
||||||
|
|
||||||
|
/* Hotplug device ids for virtio-input-host-pci passthrough. */
|
||||||
|
#define PLUG_ID_A "vmctl-a"
|
||||||
|
#define PLUG_ID_B "vmctl-b"
|
||||||
|
|
||||||
|
static const uint16_t BTN_CODES[8] = {
|
||||||
|
0x110, 0x111, 0x112, 0x113, 0x114, 0x115, 0x116, 0x117
|
||||||
|
};
|
||||||
|
|
||||||
|
static void emit(int fd, uint16_t type, uint16_t code, int32_t val) {
|
||||||
|
struct input_event e = {.type = type, .code = code, .value = val};
|
||||||
|
ssize_t r = write(fd, &e, sizeof e);
|
||||||
|
(void)r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void syn(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); }
|
||||||
|
|
||||||
|
static int uinput_create(int rel_motion, const char* name, char evdev[64]) {
|
||||||
|
int fd = open("/dev/uinput", O_RDWR | O_CLOEXEC);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
|
||||||
|
ioctl(fd, UI_SET_EVBIT, EV_SYN);
|
||||||
|
|
||||||
|
ioctl(fd, UI_SET_EVBIT, EV_KEY);
|
||||||
|
/* Keyboard keybits come from the single source of truth: every key in
|
||||||
|
* VMCTL_KEYS, so a key in the table always works through uinput too. */
|
||||||
|
for (int i = 0; i < VMCTL_KEYS_LEN; i++)
|
||||||
|
ioctl(fd, UI_SET_KEYBIT, VMCTL_KEYS[i].evdev);
|
||||||
|
for (int b = 0; b < 8; b++)
|
||||||
|
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
|
||||||
|
|
||||||
|
ioctl(fd, UI_SET_EVBIT, EV_REL);
|
||||||
|
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
|
||||||
|
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
|
||||||
|
if (rel_motion) {
|
||||||
|
ioctl(fd, UI_SET_RELBIT, REL_X);
|
||||||
|
ioctl(fd, UI_SET_RELBIT, REL_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rel_motion) {
|
||||||
|
ioctl(fd, UI_SET_EVBIT, EV_ABS);
|
||||||
|
ioctl(fd, UI_SET_ABSBIT, ABS_X);
|
||||||
|
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
|
||||||
|
|
||||||
|
struct uinput_abs_setup ax;
|
||||||
|
memset(&ax, 0, sizeof ax);
|
||||||
|
ax.code = ABS_X;
|
||||||
|
ax.absinfo.minimum = 0;
|
||||||
|
ax.absinfo.maximum = VMCTL_ABS_MAX;
|
||||||
|
ioctl(fd, UI_ABS_SETUP, &ax);
|
||||||
|
ax.code = ABS_Y;
|
||||||
|
ioctl(fd, UI_ABS_SETUP, &ax);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct uinput_setup us;
|
||||||
|
memset(&us, 0, sizeof us);
|
||||||
|
us.id.bustype = HWID_BUS;
|
||||||
|
us.id.vendor = HWID_VENDOR;
|
||||||
|
us.id.product = HWID_PRODUCT;
|
||||||
|
us.id.version = HWID_VERSION;
|
||||||
|
strncpy(us.name, name, sizeof us.name - 1);
|
||||||
|
|
||||||
|
if (ioctl(fd, UI_DEV_SETUP, &us) < 0 || ioctl(fd, UI_DEV_CREATE) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char sysname[64] = {0};
|
||||||
|
evdev[0] = '\0';
|
||||||
|
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0)
|
||||||
|
snprintf(evdev, 64, "/dev/input/%s", sysname);
|
||||||
|
|
||||||
|
if (!evdev[0]) {
|
||||||
|
ioctl(fd, UI_DEV_DESTROY);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== virtio-input-host-pci passthrough (layer 2, optional, QMP setup) ===== */
|
||||||
|
|
||||||
|
static int qmp_plug(qmp_conn* qmp, const char* bus, const char* evdev, const char* id) {
|
||||||
|
char cmd[512], resp[1024];
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"{\"execute\":\"device_del\",\"arguments\":{\"id\":\"%s\"}}", id);
|
||||||
|
qmp_exec(qmp, cmd, resp, sizeof resp);
|
||||||
|
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"{\"execute\":\"device_add\",\"arguments\":{"
|
||||||
|
"\"driver\":\"virtio-input-host-pci\","
|
||||||
|
"\"id\":\"%s\","
|
||||||
|
"\"evdev\":\"%s\","
|
||||||
|
"\"bus\":\"%s\"}}",
|
||||||
|
id, evdev, bus);
|
||||||
|
return qmp_exec(qmp, cmd, resp, sizeof resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void qmp_unplug(qmp_conn* qmp, const char* id) {
|
||||||
|
char cmd[256], resp[1024];
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"{\"execute\":\"device_del\",\"arguments\":{\"id\":\"%s\"}}", id);
|
||||||
|
qmp_exec(qmp, cmd, resp, sizeof resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== hot path (layer 1, uinput write) ===== */
|
||||||
|
|
||||||
|
static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||||
|
int fd_a = v->ui_fd_a;
|
||||||
|
int fd_b = v->ui_fd_b;
|
||||||
|
int both = (fd_b >= 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < b->count; i++) {
|
||||||
|
int code = b->ev[i].code;
|
||||||
|
int value = b->ev[i].value;
|
||||||
|
double scl = b->ev[i].scroll;
|
||||||
|
|
||||||
|
switch ((vmctl_ev_kind)b->ev[i].kind) {
|
||||||
|
case VMCTL_EV_ABS:
|
||||||
|
if (v->ptr_mode == VMCTL_PTR_REL) return -1;
|
||||||
|
emit(fd_a, EV_ABS, code == VMCTL_AXIS_X ? ABS_X : ABS_Y, value);
|
||||||
|
syn(fd_a);
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_REL: {
|
||||||
|
if (!both && v->ptr_mode == VMCTL_PTR_ABS) return -1;
|
||||||
|
int fd = both ? fd_b : fd_a;
|
||||||
|
emit(fd, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
|
||||||
|
syn(fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VMCTL_EV_BTN:
|
||||||
|
if (code < 0 || code >= 8) return -1;
|
||||||
|
emit(fd_a, EV_KEY, BTN_CODES[code], value);
|
||||||
|
syn(fd_a);
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_KEY:
|
||||||
|
emit(fd_a, EV_KEY, (uint16_t)code, value);
|
||||||
|
syn(fd_a);
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_SCROLL:
|
||||||
|
emit(fd_a, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
|
||||||
|
syn(fd_a);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void uinput_driver_close(vmctl_t* v) {
|
||||||
|
if (v->qmp) {
|
||||||
|
qmp_unplug(v->qmp, PLUG_ID_A);
|
||||||
|
if (v->ui_fd_b >= 0) qmp_unplug(v->qmp, PLUG_ID_B);
|
||||||
|
qmp_disconnect(v->qmp);
|
||||||
|
}
|
||||||
|
if (v->ui_fd_a >= 0) { ioctl(v->ui_fd_a, UI_DEV_DESTROY); close(v->ui_fd_a); }
|
||||||
|
if (v->ui_fd_b >= 0) { ioctl(v->ui_fd_b, UI_DEV_DESTROY); close(v->ui_fd_b); }
|
||||||
|
}
|
||||||
|
|
||||||
|
vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||||
|
vmctl_t* v = calloc(1, sizeof *v);
|
||||||
|
if (!v) return NULL;
|
||||||
|
v->driver = VMCTL_DRIVER_UINPUT;
|
||||||
|
v->ui_fd_a = -1;
|
||||||
|
v->ui_fd_b = -1;
|
||||||
|
|
||||||
|
char evdev_a[64], evdev_b[64];
|
||||||
|
int rel_a = (cfg->ptr_mode == VMCTL_PTR_REL);
|
||||||
|
v->ui_fd_a = uinput_create(rel_a, HWID_NAME_A, evdev_a);
|
||||||
|
if (v->ui_fd_a < 0) { free(v); return NULL; }
|
||||||
|
|
||||||
|
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
|
||||||
|
v->ui_fd_b = uinput_create(1, HWID_NAME_B, evdev_b);
|
||||||
|
if (v->ui_fd_b < 0) {
|
||||||
|
ioctl(v->ui_fd_a, UI_DEV_DESTROY);
|
||||||
|
close(v->ui_fd_a);
|
||||||
|
free(v);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg->qmp_path) {
|
||||||
|
v->qmp = qmp_connect(cfg->qmp_path);
|
||||||
|
if (!v->qmp) {
|
||||||
|
if (v->ui_fd_b >= 0) { ioctl(v->ui_fd_b, UI_DEV_DESTROY); close(v->ui_fd_b); }
|
||||||
|
ioctl(v->ui_fd_a, UI_DEV_DESTROY);
|
||||||
|
close(v->ui_fd_a);
|
||||||
|
free(v);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (cfg->input_bus && cfg->input_bus[0]) {
|
||||||
|
if (qmp_plug(v->qmp, cfg->input_bus, evdev_a, PLUG_ID_A) < 0) {
|
||||||
|
uinput_driver_close(v);
|
||||||
|
free(v);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
|
||||||
|
if (qmp_plug(v->qmp, cfg->input_bus, evdev_b, PLUG_ID_B) < 0) {
|
||||||
|
qmp_unplug(v->qmp, PLUG_ID_A);
|
||||||
|
uinput_driver_close(v);
|
||||||
|
free(v);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v->ops.send = uinput_driver_send;
|
||||||
|
v->ops.close = uinput_driver_close;
|
||||||
|
v->ptr_mode = cfg->ptr_mode;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/* open.c — handle lifecycle and the input batch API. vmctl_open dispatches to a
|
||||||
|
* driver factory by cfg->driver; vmctl_close releases via ops.close. The batch
|
||||||
|
* builders set vmctl_event.kind (the single event-kind code that drivers read),
|
||||||
|
* and the single-event wrappers are thin batches of one. */
|
||||||
|
|
||||||
|
#include "driver.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
vmctl_t* vmctl_open(const vmctl_config* cfg) {
|
||||||
|
if (!cfg) return NULL;
|
||||||
|
switch (cfg->driver) {
|
||||||
|
case VMCTL_DRIVER_QMP: return vmctl_open_qmp_driver(cfg);
|
||||||
|
case VMCTL_DRIVER_UINPUT: return vmctl_open_uinput_driver(cfg);
|
||||||
|
default: return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_close(vmctl_t* v) {
|
||||||
|
if (!v) return;
|
||||||
|
v->ops.close(v);
|
||||||
|
free(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Batch builders ===== */
|
||||||
|
|
||||||
|
void vmctl_batch_init(vmctl_batch* b) {
|
||||||
|
b->count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_batch_abs(vmctl_batch* b, int axis, int value) {
|
||||||
|
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||||
|
vmctl_event* e = &b->ev[b->count++];
|
||||||
|
e->kind = VMCTL_EV_ABS; e->code = axis; e->value = value; e->scroll = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_batch_rel(vmctl_batch* b, int axis, int delta) {
|
||||||
|
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||||
|
vmctl_event* e = &b->ev[b->count++];
|
||||||
|
e->kind = VMCTL_EV_REL; e->code = axis; e->value = delta; e->scroll = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_batch_btn(vmctl_batch* b, int btn, int down) {
|
||||||
|
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||||
|
vmctl_event* e = &b->ev[b->count++];
|
||||||
|
e->kind = VMCTL_EV_BTN; e->code = btn; e->value = down; e->scroll = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_batch_key(vmctl_batch* b, int evdev_code, int down) {
|
||||||
|
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||||
|
vmctl_event* e = &b->ev[b->count++];
|
||||||
|
e->kind = VMCTL_EV_KEY; e->code = evdev_code; e->value = down; e->scroll = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void vmctl_batch_scroll(vmctl_batch* b, int axis, double value) {
|
||||||
|
if (b->count >= VMCTL_BATCH_MAX) return;
|
||||||
|
vmctl_event* e = &b->ev[b->count++];
|
||||||
|
e->kind = VMCTL_EV_SCROLL; e->code = axis; e->value = 0; e->scroll = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||||
|
if (b->count == 0) return 0;
|
||||||
|
return v->ops.send(v, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Single-event wrappers ===== */
|
||||||
|
|
||||||
|
int vmctl_abs(vmctl_t* v, int axis, int value) {
|
||||||
|
vmctl_batch b;
|
||||||
|
vmctl_batch_init(&b);
|
||||||
|
vmctl_batch_abs(&b, axis, value);
|
||||||
|
return vmctl_batch_send(v, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_rel(vmctl_t* v, int axis, int delta) {
|
||||||
|
vmctl_batch b;
|
||||||
|
vmctl_batch_init(&b);
|
||||||
|
vmctl_batch_rel(&b, axis, delta);
|
||||||
|
return vmctl_batch_send(v, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_btn(vmctl_t* v, int btn, int down) {
|
||||||
|
vmctl_batch b;
|
||||||
|
vmctl_batch_init(&b);
|
||||||
|
vmctl_batch_btn(&b, btn, down);
|
||||||
|
return vmctl_batch_send(v, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_key(vmctl_t* v, int evdev_code, int down) {
|
||||||
|
vmctl_batch b;
|
||||||
|
vmctl_batch_init(&b);
|
||||||
|
vmctl_batch_key(&b, evdev_code, down);
|
||||||
|
return vmctl_batch_send(v, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_scroll(vmctl_t* v, int axis, double value) {
|
||||||
|
vmctl_batch b;
|
||||||
|
vmctl_batch_init(&b);
|
||||||
|
vmctl_batch_scroll(&b, axis, value);
|
||||||
|
return vmctl_batch_send(v, &b);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* power.c — QMP power/lifecycle actuation. This plane is orthogonal to the
|
||||||
|
* input driver and always rides the shared QMP channel; every entry returns -1
|
||||||
|
* when there is no connection. */
|
||||||
|
|
||||||
|
#include "driver.h"
|
||||||
|
|
||||||
|
/* QMP responses are small; a stack buffer suffices. */
|
||||||
|
static int qmp_simple(vmctl_t* v, const char* cmd) {
|
||||||
|
if (!v->qmp) return -1;
|
||||||
|
char resp[1024];
|
||||||
|
return qmp_exec(v->qmp, cmd, resp, sizeof resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int vmctl_powerdown(vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_powerdown\"}"); }
|
||||||
|
int vmctl_reset (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_reset\"}"); }
|
||||||
|
int vmctl_wakeup (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"system_wakeup\"}"); }
|
||||||
|
int vmctl_pause (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"stop\"}"); }
|
||||||
|
int vmctl_resume (vmctl_t* v) { return qmp_simple(v, "{\"execute\":\"cont\"}"); }
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/* qmp.c — AF_UNIX QMP client: connect + capability handshake, line-based recv
|
||||||
|
* with a poll timeout, and synchronous command execution. */
|
||||||
|
|
||||||
|
#include "qmp.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <poll.h>
|
||||||
|
|
||||||
|
#define QMP_TIMEOUT_MS 5000
|
||||||
|
#define QMP_BUF_SIZE 4096
|
||||||
|
|
||||||
|
struct qmp_conn {
|
||||||
|
int fd;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int recv_line(int fd, char* buf, size_t cap) {
|
||||||
|
size_t n = 0;
|
||||||
|
while (n + 1 < cap) {
|
||||||
|
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
||||||
|
if (poll(&pfd, 1, QMP_TIMEOUT_MS) <= 0) return -1;
|
||||||
|
char c;
|
||||||
|
if (read(fd, &c, 1) != 1) return -1;
|
||||||
|
buf[n++] = c;
|
||||||
|
if (c == '\n') break;
|
||||||
|
}
|
||||||
|
buf[n] = '\0';
|
||||||
|
return (int)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int send_all(int fd, const char* s, size_t len) {
|
||||||
|
while (len > 0) {
|
||||||
|
ssize_t w = write(fd, s, len);
|
||||||
|
if (w <= 0) return -1;
|
||||||
|
s += w;
|
||||||
|
len -= (size_t)w;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmp_conn* qmp_connect(const char* sock_path) {
|
||||||
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) return NULL;
|
||||||
|
|
||||||
|
struct sockaddr_un addr;
|
||||||
|
memset(&addr, 0, sizeof addr);
|
||||||
|
addr.sun_family = AF_UNIX;
|
||||||
|
strncpy(addr.sun_path, sock_path, sizeof addr.sun_path - 1);
|
||||||
|
|
||||||
|
if (connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char buf[QMP_BUF_SIZE];
|
||||||
|
if (recv_line(fd, buf, sizeof buf) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* cap_cmd = "{\"execute\":\"qmp_capabilities\"}\r\n";
|
||||||
|
if (send_all(fd, cap_cmd, strlen(cap_cmd)) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recv_line(fd, buf, sizeof buf) < 0) {
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
qmp_conn* c = malloc(sizeof *c);
|
||||||
|
if (!c) {
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
c->fd = fd;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void qmp_disconnect(qmp_conn* c) {
|
||||||
|
if (!c) return;
|
||||||
|
close(c->fd);
|
||||||
|
free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
int qmp_exec(qmp_conn* c, const char* cmd, char* resp, size_t cap) {
|
||||||
|
size_t cmdlen = strlen(cmd);
|
||||||
|
if (send_all(c->fd, cmd, cmdlen) < 0) return -1;
|
||||||
|
if (send_all(c->fd, "\r\n", 2) < 0) return -1;
|
||||||
|
|
||||||
|
char line[QMP_BUF_SIZE];
|
||||||
|
for (;;) {
|
||||||
|
if (recv_line(c->fd, line, sizeof line) < 0) return -1;
|
||||||
|
if (strstr(line, "\"return\"")) {
|
||||||
|
if (resp && cap > 0) {
|
||||||
|
strncpy(resp, line, cap - 1);
|
||||||
|
resp[cap - 1] = '\0';
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strstr(line, "\"error\"")) {
|
||||||
|
if (resp && cap > 0) {
|
||||||
|
strncpy(resp, line, cap - 1);
|
||||||
|
resp[cap - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/* qmp_driver.c — QMP input driver: serialises an input batch into a single
|
||||||
|
* input-send-event command and sends it in one round-trip. No guest driver is
|
||||||
|
* required. Switches on vmctl_ev_kind (never on magic numbers). */
|
||||||
|
|
||||||
|
#include "driver.h"
|
||||||
|
#include "keymap.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static const char* btn_names[] = {
|
||||||
|
"left", "right", "middle", "side", "extra", "forward", "back", "task"
|
||||||
|
};
|
||||||
|
#define BTN_NAMES_LEN ((int)(sizeof btn_names / sizeof btn_names[0]))
|
||||||
|
|
||||||
|
static int qmp_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||||
|
char json[8192];
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"execute\":\"input-send-event\",\"arguments\":{\"events\":[");
|
||||||
|
|
||||||
|
for (int i = 0; i < b->count; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos, ",");
|
||||||
|
|
||||||
|
int code = b->ev[i].code;
|
||||||
|
int value = b->ev[i].value;
|
||||||
|
double scl = b->ev[i].scroll;
|
||||||
|
|
||||||
|
switch ((vmctl_ev_kind)b->ev[i].kind) {
|
||||||
|
case VMCTL_EV_ABS:
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"type\":\"abs\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
|
||||||
|
code == VMCTL_AXIS_X ? "x" : "y", value);
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_REL:
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"type\":\"rel\",\"data\":{\"axis\":\"%s\",\"value\":%d}}",
|
||||||
|
code == VMCTL_AXIS_X ? "x" : "y", value);
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_BTN:
|
||||||
|
if (code < 0 || code >= BTN_NAMES_LEN) return -1;
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"type\":\"btn\",\"data\":{\"button\":\"%s\",\"down\":%s}}",
|
||||||
|
btn_names[code], value ? "true" : "false");
|
||||||
|
break;
|
||||||
|
case VMCTL_EV_KEY: {
|
||||||
|
const char* qcode = vmctl_evdev_to_qcode(code);
|
||||||
|
if (!qcode) return -1;
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"type\":\"key\",\"data\":{\"key\":{\"type\":\"qcode\","
|
||||||
|
"\"data\":\"%s\"},\"down\":%s}}",
|
||||||
|
qcode, value ? "true" : "false");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VMCTL_EV_SCROLL:
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos,
|
||||||
|
"{\"type\":\"scl\",\"data\":{\"axis\":\"%s\",\"value\":%g}}",
|
||||||
|
code == VMCTL_SCROLL_V ? "vertical" : "horizontal", scl);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += snprintf(json + pos, (int)sizeof json - pos, "]}}");
|
||||||
|
|
||||||
|
char resp[4096];
|
||||||
|
return qmp_exec(v->qmp, json, resp, sizeof resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void qmp_driver_close(vmctl_t* v) {
|
||||||
|
qmp_disconnect(v->qmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
vmctl_t* vmctl_open_qmp_driver(const vmctl_config* cfg) {
|
||||||
|
qmp_conn* qmp = qmp_connect(cfg->qmp_path);
|
||||||
|
if (!qmp) return NULL;
|
||||||
|
|
||||||
|
vmctl_t* v = calloc(1, sizeof *v);
|
||||||
|
if (!v) {
|
||||||
|
qmp_disconnect(qmp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
v->driver = VMCTL_DRIVER_QMP;
|
||||||
|
v->qmp = qmp;
|
||||||
|
v->ui_fd_a = -1;
|
||||||
|
v->ui_fd_b = -1;
|
||||||
|
v->ptr_mode = 0;
|
||||||
|
v->ops.send = qmp_driver_send;
|
||||||
|
v->ops.close = qmp_driver_close;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user