From 9b754943804e9fd28c796f7e11332b6d338d7370 Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Wed, 17 Jun 2026 12:55:36 +0300 Subject: [PATCH] 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. --- .gitignore | 4 + CMakeLists.txt | 21 +++ include/vmctl.h | 82 +++++++++++ src/vmctl/include/driver.h | 32 ++++ src/vmctl/include/keymap.h | 18 +++ src/vmctl/include/qmp.h | 14 ++ src/vmctl/keymap.c | 115 +++++++++++++++ src/vmctl/linux/uinput_driver.c | 249 ++++++++++++++++++++++++++++++++ src/vmctl/open.c | 101 +++++++++++++ src/vmctl/power.c | 18 +++ src/vmctl/qmp.c | 113 +++++++++++++++ src/vmctl/qmp_driver.c | 94 ++++++++++++ 12 files changed, 861 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 include/vmctl.h create mode 100644 src/vmctl/include/driver.h create mode 100644 src/vmctl/include/keymap.h create mode 100644 src/vmctl/include/qmp.h create mode 100644 src/vmctl/keymap.c create mode 100644 src/vmctl/linux/uinput_driver.c create mode 100644 src/vmctl/open.c create mode 100644 src/vmctl/power.c create mode 100644 src/vmctl/qmp.c create mode 100644 src/vmctl/qmp_driver.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4103d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*/ +cmake-*/ +compile* +docs/plans/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c2f9977 --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/include/vmctl.h b/include/vmctl.h new file mode 100644 index 0000000..e8302b8 --- /dev/null +++ b/include/vmctl.h @@ -0,0 +1,82 @@ +#ifndef VMCTL_H +#define VMCTL_H +#include + +/* 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 */ diff --git a/src/vmctl/include/driver.h b/src/vmctl/include/driver.h new file mode 100644 index 0000000..3f78977 --- /dev/null +++ b/src/vmctl/include/driver.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 */ diff --git a/src/vmctl/include/keymap.h b/src/vmctl/include/keymap.h new file mode 100644 index 0000000..448836a --- /dev/null +++ b/src/vmctl/include/keymap.h @@ -0,0 +1,18 @@ +#ifndef VMCTL_KEYMAP_H +#define VMCTL_KEYMAP_H +#include + +/* 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 */ diff --git a/src/vmctl/include/qmp.h b/src/vmctl/include/qmp.h new file mode 100644 index 0000000..f403e7d --- /dev/null +++ b/src/vmctl/include/qmp.h @@ -0,0 +1,14 @@ +#ifndef VMCTL_QMP_H +#define VMCTL_QMP_H +#include + +/* 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 */ diff --git a/src/vmctl/keymap.c b/src/vmctl/keymap.c new file mode 100644 index 0000000..da97e32 --- /dev/null +++ b/src/vmctl/keymap.c @@ -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 +#include + +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; +} diff --git a/src/vmctl/linux/uinput_driver.c b/src/vmctl/linux/uinput_driver.c new file mode 100644 index 0000000..d287c0e --- /dev/null +++ b/src/vmctl/linux/uinput_driver.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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; +} diff --git a/src/vmctl/open.c b/src/vmctl/open.c new file mode 100644 index 0000000..06c7c65 --- /dev/null +++ b/src/vmctl/open.c @@ -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 + +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); +} diff --git a/src/vmctl/power.c b/src/vmctl/power.c new file mode 100644 index 0000000..c31f406 --- /dev/null +++ b/src/vmctl/power.c @@ -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\"}"); } diff --git a/src/vmctl/qmp.c b/src/vmctl/qmp.c new file mode 100644 index 0000000..50113ea --- /dev/null +++ b/src/vmctl/qmp.c @@ -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 +#include +#include +#include +#include +#include + +#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; + } + } +} diff --git a/src/vmctl/qmp_driver.c b/src/vmctl/qmp_driver.c new file mode 100644 index 0000000..7f9d8bc --- /dev/null +++ b/src/vmctl/qmp_driver.c @@ -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 +#include + +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; +}