mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-25 20:36:36 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0f452fe37c
|
|||
|
85041c12ab
|
|||
|
228dc5af79
|
|||
|
d6c45ddb04
|
|||
|
929bcf0e74
|
|||
|
3142337e62
|
+18
-1
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
# Single source of truth for the version: CI passes -DVMSIG_VERSION=${TAG#v}, so the project
|
||||
# version (-> libvgpu-perception SONAME/.so version) and the .deb version come from one tag.
|
||||
set(VMSIG_VERSION "0.3.5" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
set(VMSIG_VERSION "0.3.11" CACHE STRING "Release version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
project(vmsig VERSION ${VMSIG_VERSION} LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
@@ -65,6 +65,7 @@ add_library(vmsig SHARED
|
||||
src/control/socket.c
|
||||
src/discovery/slot.c
|
||||
src/discovery/linux/host_probe.c
|
||||
src/discovery/linux/mtree.c
|
||||
src/discovery/discovery.c
|
||||
# SI input driver (vmctl), absorbed in-tree (host-only: QMP + uinput)
|
||||
src/si/input/open.c
|
||||
@@ -216,6 +217,15 @@ target_include_directories(vmsig_discoverytest PRIVATE
|
||||
target_compile_options(vmsig_discoverytest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME discovery COMMAND vmsig_discoverytest)
|
||||
|
||||
add_executable(vmsig_mtreetest src/test/test_mtree.c)
|
||||
target_link_libraries(vmsig_mtreetest PRIVATE vmsig)
|
||||
target_include_directories(vmsig_mtreetest PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/discovery/include)
|
||||
target_compile_definitions(vmsig_mtreetest PRIVATE
|
||||
FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src/test/fixtures")
|
||||
target_compile_options(vmsig_mtreetest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME mtree COMMAND vmsig_mtreetest)
|
||||
|
||||
add_executable(vmsig_daemoncfgtest
|
||||
src/test/test_daemoncfg.c
|
||||
src/daemon/config.c
|
||||
@@ -259,6 +269,13 @@ target_link_libraries(vmsig_inputobstest PRIVATE vmsig Threads::Threads)
|
||||
target_compile_options(vmsig_inputobstest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME inputobs COMMAND vmsig_inputobstest)
|
||||
|
||||
add_executable(vmsig_uinputlayouttest src/test/test_uinputlayout.c)
|
||||
target_link_libraries(vmsig_uinputlayouttest PRIVATE vmsig)
|
||||
target_include_directories(vmsig_uinputlayouttest PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/si/input/include)
|
||||
target_compile_options(vmsig_uinputlayouttest PRIVATE -Wall -Wextra)
|
||||
add_test(NAME uinputlayout COMMAND vmsig_uinputlayouttest)
|
||||
|
||||
add_executable(vmsig_memwritetest src/test/test_memwrite.c)
|
||||
target_link_libraries(vmsig_memwritetest PRIVATE vmsig Threads::Threads)
|
||||
target_include_directories(vmsig_memwritetest PRIVATE
|
||||
|
||||
+8
-9
@@ -15,10 +15,6 @@ typedef enum {
|
||||
/* 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 {
|
||||
unsigned bustype; /* HID bus type, e.g. 0x0003 (USB) */
|
||||
unsigned vendor; /* vendor id */
|
||||
@@ -31,15 +27,20 @@ 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 */
|
||||
const vmctl_uinput_id* uinput_id; /* UINPUT only; NULL = built-in defaults */
|
||||
} vmctl_config;
|
||||
|
||||
vmctl_t* vmctl_open (const vmctl_config* cfg); /* NULL on error */
|
||||
void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||
|
||||
/* Copy the host evdev node paths of the created uinput devices (UINPUT driver only).
|
||||
* a[] receives device A (keyboard), b[] receives device B (relative mouse); both are always
|
||||
* created, so count==2 in the normal case. Each buffer must be >=64 bytes. Returns the count
|
||||
* of non-empty paths filled (0/1/2), or -1 if the handle's driver is not UINPUT. Paths are
|
||||
* valid while the handle is open. */
|
||||
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]);
|
||||
|
||||
/* ===== 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 */
|
||||
@@ -61,13 +62,12 @@ void vmctl_close(vmctl_t* v); /* safe on NULL */
|
||||
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) */
|
||||
int 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);
|
||||
@@ -75,7 +75,6 @@ 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_* */
|
||||
|
||||
@@ -159,9 +159,12 @@ enum {
|
||||
* encodes vmsig_input into vmsig_event.inln.
|
||||
*
|
||||
* Pointer motion carries BOTH coordinates in ONE event (a pointer position is a single entity,
|
||||
* not two independent axis updates). btn/key/scroll stay single-valued. */
|
||||
* not two independent axis updates). btn/key/scroll stay single-valued.
|
||||
*
|
||||
* Numbering is FROZEN: an external control encodes these on the wire and is not rebuilt from
|
||||
* this header. Removing a member must NOT shift the others. */
|
||||
typedef enum {
|
||||
VMSIG_INPUT_MOVE_ABS = 0, /* absolute pointer: x,y are coordinates (0..VMCTL_ABS_MAX) */
|
||||
/* 0 reserved (was MOVE_ABS, removed) */
|
||||
VMSIG_INPUT_MOVE_REL = 1, /* relative pointer: x,y are deltas (dx,dy) */
|
||||
VMSIG_INPUT_BTN = 2, /* button: code=button, value=pressed(1)/released(0) */
|
||||
VMSIG_INPUT_KEY = 3, /* key: code=evdev code, value=pressed/released */
|
||||
@@ -175,8 +178,8 @@ typedef struct {
|
||||
uint16_t kind; /* vmsig_input_kind */
|
||||
uint16_t code; /* button / evdev code / scroll axis (NOT used by MOVE_*) */
|
||||
int32_t value; /* pressed(1)|released(0) for BTN/KEY (not used by MOVE or SCROLL) */
|
||||
int32_t x; /* MOVE_ABS: abs X (0..VMCTL_ABS_MAX); MOVE_REL: dx */
|
||||
int32_t y; /* MOVE_ABS: abs Y; MOVE_REL: dy */
|
||||
int32_t x; /* MOVE_REL: dx */
|
||||
int32_t y; /* MOVE_REL: dy */
|
||||
double scroll; /* SCROLL magnitude only */
|
||||
uint32_t flags; /* VMSIG_INPUT_F_* (see above) */
|
||||
uint32_t _pad; /* reserved; zero on emit */
|
||||
|
||||
@@ -11,7 +11,17 @@ configure)
|
||||
systemd-tmpfiles --create /usr/lib/tmpfiles.d/vmsig.conf || true
|
||||
systemctl enable vmsigd.service || true # enable, but do NOT start
|
||||
fi
|
||||
echo "vmsig: review the [grant] policy in /etc/vmsig/vmsigd.conf, then: systemctl start vmsigd" >&2
|
||||
if [ -z "$2" ]; then
|
||||
# fresh install ($2 empty): enabled but NOT started — the operator reviews the
|
||||
# grant policy before the first start.
|
||||
echo "vmsig: review the [grant] policy in /etc/vmsig/vmsigd.conf, then: systemctl start vmsigd" >&2
|
||||
else
|
||||
# upgrade ($2 = old version): a running daemon keeps the OLD in-memory image until
|
||||
# restarted — the new build is not applied automatically. Not auto-restarted here:
|
||||
# the start is gated on the grant policy, so the operator owns the moment. try-restart
|
||||
# touches the daemon only if it is currently running (leaves a stopped one alone).
|
||||
echo "vmsig: upgraded from $2 — a running daemon still runs the old build; apply with: systemctl try-restart vmsigd" >&2
|
||||
fi
|
||||
;;
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
/* Private config of the input adapter (vmctl, in-tree at src/si/input/). cfg==NULL or
|
||||
* stub!=0 => stub mode (ack without actuation). stub==0 opens vmctl_open() and actuates for
|
||||
* real. Injection is ALWAYS uinput (orphaned host uinput + external QEMU input-linux);
|
||||
* qmp_path is kept for the SERVICE path (power/lifecycle via vmctl QMP), not for injection. */
|
||||
* real. Injection is ALWAYS uinput; the created evdev nodes are forwarded into the guest by an
|
||||
* input-linux QMP object that the vmhost seam adds over its own connection (this adapter only
|
||||
* publishes the evdev paths, it never touches QMP). qmp_path is kept for the SERVICE path
|
||||
* (power/lifecycle via vmctl QMP), not for injection. */
|
||||
typedef struct {
|
||||
int stub;
|
||||
const char* qmp_path; /* for power/lifecycle (vmctl QMP); NOT input injection */
|
||||
/* On a real attach the adapter writes the uinput evdev node paths here (>=64 bytes each)
|
||||
* so the vmhost seam can bridge them via input-linux. NULL => not published; B is "" when
|
||||
* there is no second device. Buffers belong to the caller and outlive the adapter. */
|
||||
char* out_evdev_a;
|
||||
char* out_evdev_b;
|
||||
} vmsig_input_cfg;
|
||||
|
||||
/* Input event codes/contract are PUBLIC: vmsig_input / vmsig_input_kind in
|
||||
|
||||
+24
-13
@@ -22,8 +22,8 @@ typedef struct {
|
||||
int kind; /* vmsig_input_kind (for cmd==0) */
|
||||
int code; /* btn/evdev-code/scroll-axis */
|
||||
int value; /* pressed(1)/released(0) for btn/key */
|
||||
int x; /* MOVE_ABS: abs X; MOVE_REL: dx */
|
||||
int y; /* MOVE_ABS: abs Y; MOVE_REL: dy */
|
||||
int x; /* MOVE_REL: dx */
|
||||
int y; /* MOVE_REL: dy */
|
||||
double scroll;
|
||||
int noack; /* CMD_INPUT fire-and-forget: emit no ACT_ACK */
|
||||
int life_op; /* VMSIG_LIFE_* (powerdown/reset/wakeup/pause/resume) */
|
||||
@@ -39,6 +39,8 @@ struct vmsig_adapter {
|
||||
vmsig_worker* worker;
|
||||
const char* qmp_path; /* borrowed from cfg (valid through attach); SERVICE power/lifecycle */
|
||||
vmctl_t* vmctl; /* NULL in stub mode (cfg.stub) — no actuator opened */
|
||||
char* out_evdev_a; /* borrowed home for the uinput evdev path of A; NULL = not published */
|
||||
char* out_evdev_b; /* likewise for B; "" written when there is no second device */
|
||||
};
|
||||
|
||||
static int input_job(void* user, const void* reqp, void* resp) {
|
||||
@@ -55,10 +57,6 @@ static int input_job(void* user, const void* reqp, void* resp) {
|
||||
/* Pointer motion is ONE packet: both axes in a single batch -> one round-trip. */
|
||||
vmctl_batch b; vmctl_batch_init(&b);
|
||||
switch (rq->kind) {
|
||||
case VMSIG_INPUT_MOVE_ABS:
|
||||
vmctl_batch_abs(&b, VMCTL_AXIS_X, rq->x);
|
||||
vmctl_batch_abs(&b, VMCTL_AXIS_Y, rq->y);
|
||||
break;
|
||||
case VMSIG_INPUT_MOVE_REL:
|
||||
vmctl_batch_rel(&b, VMCTL_AXIS_X, rq->x);
|
||||
vmctl_batch_rel(&b, VMCTL_AXIS_Y, rq->y);
|
||||
@@ -66,7 +64,7 @@ static int input_job(void* user, const void* reqp, void* resp) {
|
||||
case VMSIG_INPUT_BTN: vmctl_batch_btn(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_KEY: vmctl_batch_key(&b, rq->code, rq->value); break;
|
||||
case VMSIG_INPUT_SCROLL: vmctl_batch_scroll(&b, rq->code, rq->scroll); break;
|
||||
default: break;
|
||||
default: break; /* unknown/0 kind (e.g. retired MOVE_ABS): no-op */
|
||||
}
|
||||
r = vmctl_batch_send(a->vmctl, &b);
|
||||
} else {
|
||||
@@ -93,7 +91,11 @@ static vmsig_adapter* in_open(const void* cfg, uint32_t endpoint) {
|
||||
if (!a) return NULL;
|
||||
a->endpoint = endpoint;
|
||||
a->stub = c ? c->stub : 1;
|
||||
if (c) a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
|
||||
if (c) {
|
||||
a->qmp_path = c->qmp_path; /* carry to attach (cfg not passed there); SERVICE power */
|
||||
a->out_evdev_a = c->out_evdev_a;
|
||||
a->out_evdev_b = c->out_evdev_b;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
@@ -104,19 +106,28 @@ static int in_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg
|
||||
if (!a->worker) return -1;
|
||||
|
||||
if (!a->stub) {
|
||||
/* armed: open the actuator. Injection is ALWAYS uinput (orphaned host uinput + external
|
||||
* QEMU input-linux). PTR_BOTH gives both pointer forms a device (A=abs tablet, B=rel
|
||||
* mouse) — the contract now promises both MOVE_ABS and MOVE_REL, so neither may be
|
||||
* disabled. qmp_path serves the SERVICE power/lifecycle path, not input injection. */
|
||||
/* armed: open the actuator. Injection is ALWAYS uinput; the resulting evdev nodes are
|
||||
* forwarded into the guest by the vmhost seam's input-linux object (published below).
|
||||
* uinput always creates two devices: A=keyboard, B=relative mouse+buttons+wheel — the
|
||||
* contract carries MOVE_REL (there is no absolute pointer). qmp_path serves the SERVICE
|
||||
* power/lifecycle path, not input injection. */
|
||||
vmctl_config vcfg;
|
||||
memset(&vcfg, 0, sizeof vcfg);
|
||||
vcfg.driver = VMCTL_DRIVER_UINPUT;
|
||||
vcfg.qmp_path = a->qmp_path;
|
||||
vcfg.input_bus = "";
|
||||
vcfg.ptr_mode = VMCTL_PTR_BOTH;
|
||||
vcfg.uinput_id = NULL; /* built-in HID identity defaults */
|
||||
a->vmctl = vmctl_open(&vcfg);
|
||||
if (!a->vmctl) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; }
|
||||
|
||||
/* Publish the created evdev node paths into the stable per-endpoint home so the vmhost
|
||||
* seam can bridge them via input-linux. The seam reads these after both attaches. */
|
||||
if (a->out_evdev_a || a->out_evdev_b) {
|
||||
char ea[64] = {0}, eb[64] = {0};
|
||||
vmctl_uinput_evdev(a->vmctl, ea, eb);
|
||||
if (a->out_evdev_a) memcpy(a->out_evdev_a, ea, sizeof ea);
|
||||
if (a->out_evdev_b) memcpy(a->out_evdev_b, eb, sizeof eb);
|
||||
}
|
||||
}
|
||||
|
||||
reg[0].fd = vmsig_worker_evfd(a->worker);
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
typedef struct {
|
||||
int stub;
|
||||
const char* qmp_path;
|
||||
/* Host->guest input bridge: evdev node paths of the uinput devices (published by the input
|
||||
* seam). When non-NULL/non-empty, on reaching READY the seam adds an input-linux QMP object
|
||||
* forwarding them into the guest (A=keyboard with grab_all, B=relative mouse). NULL/"" => no bridge
|
||||
* (stub/tests are fail-closed). Pointers are borrowed from the stable per-endpoint home and
|
||||
* outlive the adapter. */
|
||||
const char* bridge_evdev_a;
|
||||
const char* bridge_evdev_b;
|
||||
} vmsig_vmhost_cfg;
|
||||
|
||||
#endif /* VMSIG_VMHOST_H */
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
|
||||
enum { ST_STUB = 0, ST_CONNECTING, ST_NEGOTIATING, ST_READY, ST_DEAD };
|
||||
|
||||
/* Internal pend op tags for the host->guest input bridge (object_add/object_del). These are
|
||||
* NOT VMSIG_VMOP_* (which occupy 0..5) and never reach control: bridge setup is the seam's own
|
||||
* VM-substrate infrastructure, so its replies are handled silently (no ACK, no VM_LIFECYCLE). */
|
||||
#define VH_OP_BRIDGE_ADD 0x80
|
||||
#define VH_OP_BRIDGE_DEL 0x81
|
||||
|
||||
typedef struct { uint32_t id, origin, corr; uint8_t op; int used; } pend_ent;
|
||||
|
||||
struct vmsig_adapter {
|
||||
@@ -41,6 +47,13 @@ struct vmsig_adapter {
|
||||
size_t buflen;
|
||||
uint32_t next_id;
|
||||
pend_ent pend[VMHOST_MAX_PENDING];
|
||||
/* Host->guest input bridge: evdev paths borrowed from the stable per-endpoint home;
|
||||
* NULL/"" => no bridge. The *_up flags track which input-linux objects were added so
|
||||
* teardown can object_del exactly those. */
|
||||
const char* bridge_evdev_a;
|
||||
const char* bridge_evdev_b;
|
||||
int bridge_a_up;
|
||||
int bridge_b_up;
|
||||
};
|
||||
|
||||
/* ---- minimal QMP line parse (top-level keys only; full JSON — deferred) ---- */
|
||||
@@ -101,6 +114,59 @@ static pend_ent* pend_find(struct vmsig_adapter* a, uint32_t id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- host->guest input bridge (input-linux object_add/object_del) ---- */
|
||||
|
||||
/* Neutral, per-endpoint object ids — no private paths or names; evdev comes from cfg. */
|
||||
static void bridge_id(char* out, size_t cap, uint32_t ep, char ab) {
|
||||
snprintf(out, cap, "vmsig-in-%c-%u", ab, ep);
|
||||
}
|
||||
|
||||
/* Add one input-linux object forwarding an evdev node into the guest. grab_all toggles the
|
||||
* device-grab for every input-linux on this endpoint (set on A only — one is enough). The
|
||||
* reply is correlated through the existing pend[] table under VH_OP_BRIDGE_ADD and consumed
|
||||
* silently. Returns 0 on a queued write, -1 on backpressure / write failure. */
|
||||
static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int grab_all) {
|
||||
pend_ent* p = pend_alloc(a);
|
||||
if (!p) return -1;
|
||||
char id[32];
|
||||
bridge_id(id, sizeof id, a->endpoint, ab);
|
||||
uint32_t qid = ++a->next_id;
|
||||
char line[320];
|
||||
int len = snprintf(line, sizeof line,
|
||||
/* QMP object commands use HYPHENS (object-add/object-del); only the legacy
|
||||
* device_add/device_del keep underscores. Underscore here => CommandNotFound. */
|
||||
"{\"execute\":\"object-add\",\"arguments\":{\"qom-type\":\"input-linux\","
|
||||
"\"id\":\"%s\",\"evdev\":\"%s\"%s},\"id\":%u}\r\n",
|
||||
id, evdev, grab_all ? ",\"grab_all\":true" : "", qid);
|
||||
p->used = 1; p->id = qid; p->origin = 0; p->corr = 0; p->op = VH_OP_BRIDGE_ADD;
|
||||
ssize_t r = write(a->fd, line, (size_t)len);
|
||||
if (r != (ssize_t)len) { p->used = 0; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Best-effort object_del fired on teardown before the fd closes (see vh_close). No reply is
|
||||
* awaited; QEMU also drops these objects when the VM powers off, so del matters only on a
|
||||
* detach without power-off (daemon restart / endpoint move). */
|
||||
static void bridge_del_fire(struct vmsig_adapter* a, char ab) {
|
||||
char id[32];
|
||||
bridge_id(id, sizeof id, a->endpoint, ab);
|
||||
char line[160];
|
||||
int len = snprintf(line, sizeof line,
|
||||
"{\"execute\":\"object-del\",\"arguments\":{\"id\":\"%s\"}}\r\n", id);
|
||||
ssize_t r = write(a->fd, line, (size_t)len);
|
||||
(void)r;
|
||||
}
|
||||
|
||||
/* Add the bridge objects upon reaching READY. A is added with grab_all; B (mouse) without. */
|
||||
static void bridge_on_ready(struct vmsig_adapter* a) {
|
||||
if (a->bridge_evdev_a && a->bridge_evdev_a[0]) {
|
||||
if (bridge_add(a, 'a', a->bridge_evdev_a, 1) == 0) a->bridge_a_up = 1;
|
||||
}
|
||||
if (a->bridge_evdev_b && a->bridge_evdev_b[0]) {
|
||||
if (bridge_add(a, 'b', a->bridge_evdev_b, 0) == 0) a->bridge_b_up = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- emission of neutral UP events ---- */
|
||||
static void emit_vm(struct vmsig_adapter* a, uint32_t state, uint32_t origin, uint32_t corr) {
|
||||
vmsig_vm_state vs = { state, 0 };
|
||||
@@ -142,7 +208,11 @@ static void handle_line(struct vmsig_adapter* a, const char* line) {
|
||||
}
|
||||
break;
|
||||
case ST_NEGOTIATING:
|
||||
if (strstr(line, "\"return\"")) { a->st = ST_READY; emit_seam(a, VMSIG_EV_SEAM_UP); }
|
||||
if (strstr(line, "\"return\"")) {
|
||||
a->st = ST_READY;
|
||||
emit_seam(a, VMSIG_EV_SEAM_UP);
|
||||
bridge_on_ready(a); /* forward the host uinput evdev nodes into the guest */
|
||||
}
|
||||
break;
|
||||
case ST_READY:
|
||||
if (strstr(line, "\"event\"")) {
|
||||
@@ -155,7 +225,12 @@ static void handle_line(struct vmsig_adapter* a, const char* line) {
|
||||
long id = jnum(line, "\"id\"");
|
||||
pend_ent* p = id >= 0 ? pend_find(a, (uint32_t)id) : NULL;
|
||||
if (p) {
|
||||
if (p->op == VMSIG_VMOP_QUERY && strstr(line, "\"return\"")) {
|
||||
if (p->op == VH_OP_BRIDGE_ADD || p->op == VH_OP_BRIDGE_DEL) {
|
||||
/* Bridge infrastructure: never surfaces to control. Log a failed add so the
|
||||
* stand can see it; otherwise silent. */
|
||||
if (p->op == VH_OP_BRIDGE_ADD && strstr(line, "\"error\""))
|
||||
fprintf(stderr, "vmsig vmhost: input-linux object-add failed: %s\n", line);
|
||||
} else if (p->op == VMSIG_VMOP_QUERY && strstr(line, "\"return\"")) {
|
||||
char stbuf[32]; uint32_t s = VMSIG_VM_UNKNOWN;
|
||||
if (jstr(line, "\"status\"", stbuf, sizeof stbuf)) s = status_state(stbuf);
|
||||
emit_vm(a, s, p->origin, p->corr); /* addressed reply */
|
||||
@@ -184,6 +259,7 @@ static vmsig_adapter* vh_open(const void* cfg, uint32_t endpoint) {
|
||||
a->endpoint = endpoint;
|
||||
a->qmp_path = (c && c->qmp_path && c->qmp_path[0]) ? c->qmp_path : NULL;
|
||||
a->stub = (a->qmp_path == NULL); /* path given => armed, otherwise stub */
|
||||
if (c) { a->bridge_evdev_a = c->bridge_evdev_a; a->bridge_evdev_b = c->bridge_evdev_b; }
|
||||
a->fd = -1;
|
||||
a->cur = VMSIG_VM_RUNNING;
|
||||
return a;
|
||||
@@ -300,6 +376,12 @@ static int vh_submit(vmsig_adapter* a, const vmsig_event* ev) {
|
||||
|
||||
static void vh_close(vmsig_adapter* a) {
|
||||
if (!a) return;
|
||||
/* Best-effort teardown of the input-linux bridge while the connection is still live: fire
|
||||
* object_del for the objects we added, with no round-trip (the fd closes right after). */
|
||||
if (a->st == ST_READY && a->fd >= 0) {
|
||||
if (a->bridge_a_up) bridge_del_fire(a, 'a');
|
||||
if (a->bridge_b_up) bridge_del_fire(a, 'b');
|
||||
}
|
||||
if (a->fd >= 0) close(a->fd);
|
||||
free(a);
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ static int on_event(void* user, const vmsig_event* ev) {
|
||||
in.prio = VMSIG_PRIO_HIGH; in.endpoint = 0; in.corr = 0xC0FFEEu;
|
||||
in.payload.flags = VMSIG_PL_INLINE;
|
||||
vmsig_input act; memset(&act, 0, sizeof act); /* neutral public input contract */
|
||||
act.kind = VMSIG_INPUT_MOVE_ABS; act.x = 100; act.y = 100; /* demo: abs pointer (100,100) */
|
||||
act.kind = VMSIG_INPUT_MOVE_REL; act.x = 5; act.y = 5; /* demo: relative move (dx=5,dy=5) */
|
||||
memcpy(in.inln, &act, sizeof act);
|
||||
printf(" DOWN CMD_INPUT MOVE_ABS x=100 y=100 corr=0x%X\n", (unsigned)in.corr);
|
||||
printf(" DOWN CMD_INPUT MOVE_REL dx=5 dy=5 corr=0x%X\n", (unsigned)in.corr);
|
||||
vmsig_inproc_send(d->ctl, &in);
|
||||
|
||||
vmsig_event vm;
|
||||
|
||||
@@ -56,6 +56,10 @@ struct vmsig_discovery {
|
||||
* keep pointers, and detach is deferred, so this must outlive the candidate. Overwritten
|
||||
* only on the NEXT attach to the endpoint, which never races a still-open prior adapter. */
|
||||
vmsig_host_facts ep_facts[VMSIG_SLOT_COUNT];
|
||||
/* Stable per-endpoint home for the uinput evdev paths of the input bridge. The input seam
|
||||
* writes these at attach; the vmhost seam borrows them to add input-linux objects. Same
|
||||
* lifetime discipline as ep_facts (outlives the deferred adapter reap). */
|
||||
struct { char evdev_a[64]; char evdev_b[64]; } ep_bridge[VMSIG_SLOT_COUNT];
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
@@ -265,17 +269,27 @@ static void bootstrap_scan(vmsig_discovery* d) {
|
||||
|
||||
static int default_attach(void* ud, vmsig_core* core, uint32_t vmid, uint32_t endpoint,
|
||||
const vmsig_host_facts* f) {
|
||||
(void)ud; (void)vmid;
|
||||
(void)vmid;
|
||||
vmsig_discovery* d = ud; /* default sink carries the discovery handle (ep_bridge home) */
|
||||
char* ev_a = d ? d->ep_bridge[endpoint].evdev_a : NULL;
|
||||
char* ev_b = d ? d->ep_bridge[endpoint].evdev_b : NULL;
|
||||
if (d) { ev_a[0] = '\0'; ev_b[0] = '\0'; } /* clear stale paths from a prior attach */
|
||||
|
||||
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
|
||||
mc.stub = 0; mc.ram_path = f->ram_path; mc.low = f->low; mc.ro_fd = -1;
|
||||
vmsig_vmhost_cfg vh; memset(&vh, 0, sizeof vh);
|
||||
vh.stub = 0; vh.qmp_path = f->qmp_path;
|
||||
vmsig_input_cfg in; memset(&in, 0, sizeof in);
|
||||
in.stub = 0; in.qmp_path = NULL; /* input is uinput; power/lifecycle via the vmhost seam */
|
||||
/* input is uinput; power/lifecycle via the vmhost seam. The adapter publishes its uinput
|
||||
* evdev paths into ep_bridge so the vmhost seam can forward them via input-linux. */
|
||||
in.stub = 0; in.qmp_path = NULL; in.out_evdev_a = ev_a; in.out_evdev_b = ev_b;
|
||||
vmsig_vmhost_cfg vh; memset(&vh, 0, sizeof vh);
|
||||
/* vmhost borrows the (now-populated) evdev paths to add the input-linux bridge at READY. */
|
||||
vh.stub = 0; vh.qmp_path = f->qmp_path; vh.bridge_evdev_a = ev_a; vh.bridge_evdev_b = ev_b;
|
||||
|
||||
/* Order matters: input attaches BEFORE vmhost so the evdev paths are written into ep_bridge
|
||||
* before the vmhost seam reads them (READY is async and always later than both attaches). */
|
||||
if (vmsig_core_add_adapter(core, vmsig_memctx_ops(), &mc, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vh, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_input_ops(), &in, endpoint) < 0) goto fail;
|
||||
if (vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vh, endpoint) < 0) goto fail;
|
||||
return 0;
|
||||
fail:
|
||||
vmsig_core_detach_endpoint(core, endpoint); /* roll back any partial trio (deferred) */
|
||||
@@ -319,7 +333,7 @@ vmsig_discovery* vmsig_discovery_new(vmsig_core* core,
|
||||
pve_conf ? pve_conf : "/etc/pve/qemu-server",
|
||||
qmp_dir ? qmp_dir : "/var/run/qemu-server");
|
||||
if (sink) d->sink = *sink;
|
||||
else { d->sink.attach = default_attach; d->sink.detach = default_detach; d->sink.ud = NULL; }
|
||||
else { d->sink.attach = default_attach; d->sink.detach = default_detach; d->sink.ud = d; }
|
||||
|
||||
slot_load(&d->slots, d->persist ? d->slots_path : NULL);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef VMSIG_MTREE_H
|
||||
#define VMSIG_MTREE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Derive the below-4G split (== vmie `low`: low-RAM GPA bound AND high-RAM file offset)
|
||||
* from `info mtree -f` text. Operates on the system flatview only. FAIL-CLOSED: 0 if the
|
||||
* split cannot be derived with confidence. `text` is plain UTF-8 with real '\n'
|
||||
* (the caller un-escapes the QMP JSON string first). */
|
||||
uint64_t mtree_low_split(const char* text);
|
||||
|
||||
#endif /* VMSIG_MTREE_H */
|
||||
@@ -5,6 +5,7 @@
|
||||
* leaves ok=0 (the VM is not brought up rather than guessed). */
|
||||
#define _GNU_SOURCE
|
||||
#include "host_probe.h"
|
||||
#include "mtree.h" /* mtree_low_split */
|
||||
#include "vmsig_event.h" /* VMSIG_VM_* */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -174,29 +175,39 @@ static int qmp_status_word(const char* buf) {
|
||||
return VMSIG_VM_UNKNOWN;
|
||||
}
|
||||
|
||||
/* Derive the below-4G split from `info mtree` text: the size of the RAM region whose guest
|
||||
* physical range starts at address 0. Standard QEMU split-RAM layout puts low RAM at
|
||||
* [0, low) and high RAM above 4G at file offset @low. FAIL-CLOSED: 0 if not found.
|
||||
* NOTE: parses HMP text (not a stable QMP schema) — verify against real `info mtree` output. */
|
||||
static uint64_t mtree_low(const char* ret) {
|
||||
/* The return is a JSON string; lines inside are escaped "\n". Scan for the GPA-0 ram run:
|
||||
* " 0000000000000000-<end16> (prio N, ram): ..." */
|
||||
const char* p = ret;
|
||||
while ((p = strstr(p, "0000000000000000-")) != NULL) {
|
||||
const char* end_hex = p + 17; /* 16 zeros + '-' */
|
||||
char* stop = NULL;
|
||||
unsigned long long end = strtoull(end_hex, &stop, 16);
|
||||
/* the descriptor after the range must mark it RAM (not the i/o "system" root) */
|
||||
const char* tail = stop ? stop : end_hex;
|
||||
const char* nl = strstr(tail, "\\n");
|
||||
const char* lim = nl ? nl : (tail + 64);
|
||||
int is_ram = 0;
|
||||
for (const char* q = tail; q < lim && *q; q++)
|
||||
if (!strncmp(q, "ram)", 4)) { is_ram = 1; break; }
|
||||
if (is_ram && end > 0 && end != ~0ull) return end + 1ull; /* [0, end] => low=end+1 */
|
||||
p = end_hex;
|
||||
/* Extract the JSON string value of "return" from an HMP-over-QMP reply and decode its
|
||||
* transport escapes (\n \t \" \\) in place into a NUL-terminated plain-text buffer. The
|
||||
* `info mtree -f` output is one JSON string with embedded escaped newlines; un-escaping is
|
||||
* a transport detail of HMP-over-QMP and belongs here (next to the QMP code), so the split
|
||||
* parser (mtree_low_split) can work on human-readable text with real '\n'. The decode never
|
||||
* grows the buffer (every escape shortens it), so it writes into `out` (>= strlen(buf)+1).
|
||||
* Returns 1 on success, 0 if no "return" string is present. */
|
||||
static int qmp_return_plain(const char* buf, char* out, size_t cap) {
|
||||
const char* r = strstr(buf, "\"return\"");
|
||||
if (!r) return 0;
|
||||
r = strchr(r, ':'); if (!r) return 0;
|
||||
r = strchr(r, '"'); if (!r) return 0; /* opening quote of the string value */
|
||||
r++;
|
||||
size_t o = 0;
|
||||
for (; *r && o + 1 < cap; r++) {
|
||||
char c = *r;
|
||||
if (c == '"') break; /* closing quote */
|
||||
if (c == '\\' && r[1]) {
|
||||
r++;
|
||||
switch (*r) {
|
||||
case 'n': c = '\n'; break;
|
||||
case 't': c = '\t'; break;
|
||||
case 'r': c = '\r'; break;
|
||||
case '"': c = '"'; break;
|
||||
case '\\': c = '\\'; break;
|
||||
case '/': c = '/'; break;
|
||||
default: c = *r; break; /* unknown escape: take it literally */
|
||||
}
|
||||
}
|
||||
out[o++] = c;
|
||||
}
|
||||
return 0;
|
||||
out[o] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int hp_live(const struct vmsig_host_probe* p, vmsig_host_facts* io) {
|
||||
@@ -221,7 +232,9 @@ static int hp_live(const struct vmsig_host_probe* p, vmsig_host_facts* io) {
|
||||
if (qmp_cmd(fd,
|
||||
"{\"execute\":\"human-monitor-command\","
|
||||
"\"arguments\":{\"command-line\":\"info mtree -f\"}}\n", buf, 256 * 1024) == 1) {
|
||||
io->low = mtree_low(buf);
|
||||
/* un-escape the HMP string in place (it only shrinks), then parse the split */
|
||||
if (qmp_return_plain(buf, buf, 256 * 1024))
|
||||
io->low = mtree_low_split(buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/* mtree.c — derive the below-4G split (vmie `low`) from `info mtree -f` text.
|
||||
*
|
||||
* `low` is one number with two meanings (see vmie low_segs): the GPA bound of low-RAM
|
||||
* ([0,low) maps 1:1 to file[0,low)) AND the file offset at which RAM resumes above 4 GiB
|
||||
* (GPA 4GiB -> file[low]). The robust signal for it is therefore the `@<file_off>` suffix
|
||||
* of the high-RAM ram region (GPA >= 4 GiB): that offset IS `low` by construction.
|
||||
*
|
||||
* Low-RAM below 4 GiB is fragmented (Hyper-V synic overlays, smbase/tseg blackhole i/o
|
||||
* holes, rom holes), so "end of the first contiguous ram run" is NOT a reliable split.
|
||||
* We never trust it. Primary signal: high-RAM `@offset`. Cross-validator / fallback:
|
||||
* the start GPA of the first non-ram region at or above the standard PCI-hole base
|
||||
* (0x80000000) — the bottom of the 4 GiB PCI hole, which equals `low` for the classic
|
||||
* single-`low` layout. The two must agree when both are present; otherwise fail-closed.
|
||||
*
|
||||
* Pure text, line by line, no allocation beyond the input, no I/O. FAIL-CLOSED: any
|
||||
* unexpected/incomplete input yields 0 ("not found"); 0 is reserved for that. */
|
||||
#include "mtree.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Standard QEMU/i440fx/q35 PCI-hole base (bottom of the 4 GiB hole). Used ONLY as the
|
||||
* lower cutoff for the cross-validator/fallback, never hardcoded as the answer. */
|
||||
#define PCI_HOLE_BASE 0x80000000ull
|
||||
/* 4 GiB: high-RAM (the ram region carrying `@low`) starts at or above this GPA. */
|
||||
#define RAM_HIGH_BASE 0x100000000ull
|
||||
|
||||
/* Parse exactly `n` hex digits at p into *out. Returns the char past the last digit, or
|
||||
* NULL if there are not n hex digits (no partial consume). */
|
||||
static const char* parse_hexn(const char* p, int n, uint64_t* out) {
|
||||
uint64_t v = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
char c = p[i];
|
||||
unsigned d;
|
||||
if (c >= '0' && c <= '9') d = (unsigned)(c - '0');
|
||||
else if (c >= 'a' && c <= 'f') d = (unsigned)(c - 'a' + 10);
|
||||
else if (c >= 'A' && c <= 'F') d = (unsigned)(c - 'A' + 10);
|
||||
else return NULL;
|
||||
v = (v << 4) | d;
|
||||
}
|
||||
*out = v;
|
||||
return p + n;
|
||||
}
|
||||
|
||||
/* One region line of a flatview body, e.g.
|
||||
* " 0000000100000000-000000027fffffff (prio 0, ram): ram0 @0000000080000000 KVM"
|
||||
* Two leading spaces, 16-hex start, '-', 16-hex end, " (prio <N>, <flag>): <rest>".
|
||||
* Fills *start_gpa, *is_ram and, when present in <rest>, *file_off (with *has_off=1).
|
||||
* Returns 1 on a well-formed region line, 0 otherwise (not a region line for us). */
|
||||
typedef struct {
|
||||
uint64_t start_gpa;
|
||||
int is_ram; /* flag is exactly "ram" (not ramd/romd/rom/i/o/container) */
|
||||
int has_off; /* a "@<hex>" suffix was present in the descriptor */
|
||||
uint64_t file_off; /* value of that suffix */
|
||||
} region_line;
|
||||
|
||||
static int parse_region_line(const char* line, const char* nl, region_line* out) {
|
||||
/* leading " " then 16 hex, '-', 16 hex */
|
||||
if (line[0] != ' ' || line[1] != ' ') return 0;
|
||||
const char* p = line + 2;
|
||||
uint64_t start, end;
|
||||
p = parse_hexn(p, 16, &start);
|
||||
if (!p || *p != '-') return 0;
|
||||
p++;
|
||||
p = parse_hexn(p, 16, &end);
|
||||
if (!p) return 0;
|
||||
|
||||
/* " (prio <N>, <flag>):" — find the flag between ", " and ")". */
|
||||
if (strncmp(p, " (prio ", 7) != 0) return 0;
|
||||
const char* comma = memchr(p, ',', (size_t)(nl - p));
|
||||
if (!comma) return 0;
|
||||
const char* flag = comma + 1;
|
||||
while (flag < nl && *flag == ' ') flag++;
|
||||
const char* rparen = memchr(flag, ')', (size_t)(nl - flag));
|
||||
if (!rparen) return 0;
|
||||
size_t flen = (size_t)(rparen - flag);
|
||||
|
||||
out->start_gpa = start;
|
||||
out->is_ram = (flen == 3 && strncmp(flag, "ram", 3) == 0) ? 1 : 0;
|
||||
|
||||
/* optional "@<hex>" anywhere in the descriptor tail (after "): "). */
|
||||
out->has_off = 0;
|
||||
out->file_off = 0;
|
||||
const char* at = memchr(rparen, '@', (size_t)(nl - rparen));
|
||||
if (at) {
|
||||
char* stop = NULL;
|
||||
unsigned long long v = strtoull(at + 1, &stop, 16);
|
||||
if (stop && stop != at + 1) { out->has_off = 1; out->file_off = (uint64_t)v; }
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Locate the system flatview body: the lines AFTER " Root memory region: system" up to
|
||||
* the next "FlatView #" (or EOF). Returns the body start, sets *body_end; NULL if absent. */
|
||||
static const char* find_system_flatview(const char* text, const char** body_end) {
|
||||
const char* anchor = "Root memory region: system";
|
||||
const char* p = text;
|
||||
while ((p = strstr(p, anchor)) != NULL) {
|
||||
/* The root name must end the token (CR/LF/space/EOF) — reject "system.flash0" etc.,
|
||||
* and reject roots that merely contain the word elsewhere. QEMU's HMP output is
|
||||
* CRLF, so the byte after "system" is '\r'; accept it (LF-only input also works). */
|
||||
const char* after = p + strlen(anchor);
|
||||
if (*after == '\n' || *after == '\0' || *after == ' ' || *after == '\r') {
|
||||
const char* body = strchr(p, '\n');
|
||||
if (!body) return NULL;
|
||||
body++; /* first region line */
|
||||
const char* fv = strstr(body, "\nFlatView #");
|
||||
*body_end = fv ? fv + 1 : (body + strlen(body));
|
||||
return body;
|
||||
}
|
||||
p = after;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Primary signal: file offset (`@hex`) of the first ram region whose start GPA >= 4 GiB.
|
||||
* Returns 1 and sets *off when found, 0 otherwise. */
|
||||
static int high_ram_offset(const char* body, const char* end, uint64_t* off) {
|
||||
const char* p = body;
|
||||
while (p < end) {
|
||||
const char* nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char* line_end = nl ? nl : end;
|
||||
region_line r;
|
||||
if (parse_region_line(p, line_end, &r) &&
|
||||
r.is_ram && r.start_gpa >= RAM_HIGH_BASE && r.has_off) {
|
||||
*off = r.file_off;
|
||||
return 1;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Cross-validator / fallback: start GPA of the first non-ram region at or above the
|
||||
* PCI-hole base (the bottom of the 4 GiB hole == low for the classic layout). Returns 1
|
||||
* and sets *base when found, 0 otherwise. Blackhole holes below 0x80000000 are skipped
|
||||
* by the lower cutoff. */
|
||||
static int pci_hole_start(const char* body, const char* end, uint64_t* base) {
|
||||
const char* p = body;
|
||||
while (p < end) {
|
||||
const char* nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char* line_end = nl ? nl : end;
|
||||
region_line r;
|
||||
if (parse_region_line(p, line_end, &r) &&
|
||||
!r.is_ram && r.start_gpa >= PCI_HOLE_BASE && r.start_gpa < RAM_HIGH_BASE) {
|
||||
*base = r.start_gpa;
|
||||
return 1;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t mtree_low_split(const char* text) {
|
||||
if (!text) return 0;
|
||||
|
||||
const char* body_end = NULL;
|
||||
const char* body = find_system_flatview(text, &body_end);
|
||||
if (!body) return 0; /* no system AS => fail-closed */
|
||||
|
||||
uint64_t off = 0, base = 0;
|
||||
int have_off = high_ram_offset(body, body_end, &off);
|
||||
int have_base = pci_hole_start(body, body_end, &base);
|
||||
|
||||
if (have_off) {
|
||||
if (off == 0 || off == ~0ull) return 0; /* degenerate offset */
|
||||
/* cross-validate against the PCI-hole base when we have one */
|
||||
if (have_base && base != off) return 0; /* layout anomaly => fail-closed */
|
||||
return off; /* primary signal */
|
||||
}
|
||||
|
||||
/* No high-RAM (guest RAM all below 4 GiB): fall back to the PCI-hole base, but only
|
||||
* at or above the standard base so blackhole holes can never be mistaken for it. */
|
||||
if (have_base && base >= PCI_HOLE_BASE) return base;
|
||||
|
||||
return 0; /* nothing trustworthy */
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* 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_REL, VMCTL_EV_BTN, VMCTL_EV_KEY, VMCTL_EV_SCROLL
|
||||
} vmctl_ev_kind;
|
||||
|
||||
typedef struct {
|
||||
@@ -20,9 +20,10 @@ 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 */
|
||||
int ui_fd_a; /* uinput driver: device A (keyboard); -1 for QMP */
|
||||
int ui_fd_b; /* uinput driver: device B (relative mouse); -1 */
|
||||
char ui_evdev_a[64]; /* uinput driver: /dev/input/eventN of A ("" if none) */
|
||||
char ui_evdev_b[64]; /* uinput driver: /dev/input/eventN of B ("" if none) */
|
||||
|
||||
/* Held-state receipt: key/btn down-bits as THIS handle last actuated them
|
||||
* (not guest truth). Written only after a successful send in
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef VMCTL_UINPUT_LAYOUT_H
|
||||
#define VMCTL_UINPUT_LAYOUT_H
|
||||
#include "vmctl.h"
|
||||
|
||||
/* uinput_layout.h — DECLARATIVE capability split for the uinput driver, kept pure (no ioctl)
|
||||
* so it is unit-testable without /dev/uinput. The roles are passed into the driver as DATA, not
|
||||
* inferred as a side effect of one another; the hot path's button/wheel carrier follows the same
|
||||
* rule.
|
||||
*
|
||||
* Layout is CONSTANT (no absolute pointer): device A = keyboard only; device B = relative pointer
|
||||
* + mouse buttons + scroll wheel. Buttons + wheel ride device B (the relative-pointer carrier). */
|
||||
|
||||
typedef struct {
|
||||
int present; /* 1 if this device is created */
|
||||
int rel_motion; /* advertise relative X/Y (no device advertises abs) */
|
||||
int want_keyboard; /* advertise the keyboard keymap */
|
||||
int want_buttons; /* advertise the 8 mouse buttons */
|
||||
int want_wheel; /* advertise REL_WHEEL / REL_HWHEEL */
|
||||
} uinput_role;
|
||||
|
||||
/* Fill role_a/role_b with the constant layout. *btn_on_b is always 1: the button/wheel carrier
|
||||
* on the hot path is device B (the relative-pointer device). Both devices are always present. */
|
||||
static inline void vmctl_uinput_layout(uinput_role* role_a, uinput_role* role_b, int* btn_on_b) {
|
||||
role_a->present = 1;
|
||||
role_a->rel_motion = 0; /* keyboard-only: no pointer on A */
|
||||
role_a->want_keyboard = 1;
|
||||
role_a->want_buttons = 0;
|
||||
role_a->want_wheel = 0;
|
||||
|
||||
role_b->present = 1;
|
||||
role_b->rel_motion = 1;
|
||||
role_b->want_keyboard = 0;
|
||||
role_b->want_buttons = 1;
|
||||
role_b->want_wheel = 1;
|
||||
|
||||
if (btn_on_b) *btn_on_b = 1;
|
||||
}
|
||||
|
||||
#endif /* VMCTL_UINPUT_LAYOUT_H */
|
||||
@@ -9,12 +9,19 @@
|
||||
* (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). */
|
||||
* uinput != virtio. The created uinput evdev nodes are forwarded into the guest
|
||||
* by an input-linux QMP object that the signaling vmhost seam adds over its own
|
||||
* connection (the evdev paths are exported via vmctl_uinput_evdev). The driver
|
||||
* switches on vmctl_ev_kind (never on magic numbers).
|
||||
*
|
||||
* Capability layout (constant, no absolute pointer): device A = keyboard only;
|
||||
* device B = relative pointer + mouse buttons + scroll wheel. Buttons/wheel ride
|
||||
* device B (the relative-pointer carrier). This split is DECLARATIVE: the roles
|
||||
* (want_buttons/want_wheel/rel_motion) are passed into uinput_create as data. */
|
||||
|
||||
#include "driver.h"
|
||||
#include "keymap.h"
|
||||
#include "uinput_layout.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -22,6 +29,7 @@
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
@@ -50,41 +58,35 @@ static void emit(int fd, uint16_t type, uint16_t code, int32_t val) {
|
||||
|
||||
static void syn(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); }
|
||||
|
||||
static int uinput_create(int rel_motion, const vmctl_uinput_id* id, const char* name, char evdev[64]) {
|
||||
/* The declarative per-device role (uinput_role) and the constant A/B split live in
|
||||
* uinput_layout.h so the layout is unit-testable without /dev/uinput. */
|
||||
static int uinput_create(const uinput_role* role, const vmctl_uinput_id* id,
|
||||
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 (role->want_keyboard) {
|
||||
/* 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);
|
||||
}
|
||||
if (role->want_buttons) {
|
||||
for (int b = 0; b < 8; b++)
|
||||
ioctl(fd, UI_SET_KEYBIT, (int)BTN_CODES[b]);
|
||||
}
|
||||
|
||||
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);
|
||||
if (role->want_wheel || role->rel_motion) ioctl(fd, UI_SET_EVBIT, EV_REL);
|
||||
if (role->want_wheel) {
|
||||
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
|
||||
}
|
||||
if (role->rel_motion) {
|
||||
ioctl(fd, UI_SET_RELBIT, REL_X);
|
||||
ioctl(fd, UI_SET_RELBIT, REL_Y);
|
||||
}
|
||||
|
||||
struct uinput_setup us;
|
||||
@@ -102,8 +104,28 @@ static int uinput_create(int rel_motion, const vmctl_uinput_id* id, const char*
|
||||
|
||||
char sysname[64] = {0};
|
||||
evdev[0] = '\0';
|
||||
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0)
|
||||
snprintf(evdev, 64, "/dev/input/%s", sysname);
|
||||
/* UI_GET_SYSNAME returns the input-class directory name (e.g. "input174"), NOT the usable
|
||||
* device node: a bare /dev/input/<sysname> does not exist (QEMU input-linux fails to open
|
||||
* it). The evdev character device is /dev/input/eventN, exposed as the "event*" child of
|
||||
* /sys/class/input/<sysname>/ — resolve it there. */
|
||||
if (ioctl(fd, UI_GET_SYSNAME(sizeof sysname), sysname) >= 0) {
|
||||
char dpath[128];
|
||||
snprintf(dpath, sizeof dpath, "/sys/class/input/%s", sysname);
|
||||
DIR* dir = opendir(dpath);
|
||||
if (dir) {
|
||||
struct dirent* de;
|
||||
while ((de = readdir(dir)) != NULL)
|
||||
if (strncmp(de->d_name, "event", 5) == 0) {
|
||||
size_t nl = strlen(de->d_name);
|
||||
if (nl + 11 < 64) { /* "/dev/input/" is 11 chars + name + NUL */
|
||||
memcpy(evdev, "/dev/input/", 11);
|
||||
memcpy(evdev + 11, de->d_name, nl + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!evdev[0]) {
|
||||
ioctl(fd, UI_DEV_DESTROY);
|
||||
@@ -144,7 +166,10 @@ static void qmp_unplug(qmp_conn* qmp, const char* id) {
|
||||
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);
|
||||
/* Relative motion, mouse buttons and the scroll wheel all ride device B (the relative-pointer
|
||||
* carrier), matching the constant layout used at create time; the keyboard rides device A. */
|
||||
int fd_rel = fd_b;
|
||||
int fd_btn = fd_b;
|
||||
|
||||
for (int i = 0; i < b->count; i++) {
|
||||
int code = b->ev[i].code;
|
||||
@@ -152,30 +177,23 @@ static int uinput_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||
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);
|
||||
emit(fd_rel, EV_REL, code == VMCTL_AXIS_X ? REL_X : REL_Y, value);
|
||||
syn(fd_rel);
|
||||
break;
|
||||
}
|
||||
case VMCTL_EV_BTN:
|
||||
if (code < 0 || code >= 8) return -1;
|
||||
emit(fd_a, EV_KEY, BTN_CODES[code], value);
|
||||
syn(fd_a);
|
||||
emit(fd_btn, EV_KEY, BTN_CODES[code], value);
|
||||
syn(fd_btn);
|
||||
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);
|
||||
emit(fd_btn, EV_REL, code == VMCTL_SCROLL_V ? REL_WHEEL : REL_HWHEEL, (int32_t)scl);
|
||||
syn(fd_btn);
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
@@ -212,13 +230,13 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
const char* base = (cfg->uinput_id && cfg->uinput_id->name && cfg->uinput_id->name[0])
|
||||
? cfg->uinput_id->name : NULL;
|
||||
|
||||
/* A/B suffix is added by the library only when two devices are created
|
||||
* (VMCTL_PTR_BOTH) and only over a caller-supplied base name. */
|
||||
/* Two devices are always created (A=keyboard, B=relative mouse); the A/B suffix is added by
|
||||
* the library over a caller-supplied base name. */
|
||||
char name_a[UINPUT_MAX_NAME_SIZE];
|
||||
char name_b[UINPUT_MAX_NAME_SIZE];
|
||||
const char* dev_a = base ? base : HWID_NAME_A;
|
||||
const char* dev_b = HWID_NAME_B;
|
||||
if (cfg->ptr_mode == VMCTL_PTR_BOTH && base) {
|
||||
if (base) {
|
||||
int base_max = (int)(sizeof name_a - 1 /*NUL*/ - 2 /*"-A"*/);
|
||||
snprintf(name_a, sizeof name_a, "%.*s-A", base_max, base);
|
||||
snprintf(name_b, sizeof name_b, "%.*s-B", base_max, base);
|
||||
@@ -227,18 +245,22 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
}
|
||||
|
||||
char evdev_a[64], evdev_b[64];
|
||||
int rel_a = (cfg->ptr_mode == VMCTL_PTR_REL);
|
||||
v->ui_fd_a = uinput_create(rel_a, id, dev_a, evdev_a);
|
||||
if (v->ui_fd_a < 0) { free(v); return NULL; }
|
||||
uinput_role role_a, role_b;
|
||||
vmctl_uinput_layout(&role_a, &role_b, NULL); /* constant A/B split */
|
||||
|
||||
if (cfg->ptr_mode == VMCTL_PTR_BOTH) {
|
||||
v->ui_fd_b = uinput_create(1, id, dev_b, evdev_b);
|
||||
v->ui_fd_a = uinput_create(&role_a, id, dev_a, evdev_a);
|
||||
if (v->ui_fd_a < 0) { free(v); return NULL; }
|
||||
memcpy(v->ui_evdev_a, evdev_a, sizeof v->ui_evdev_a);
|
||||
|
||||
if (role_b.present) {
|
||||
v->ui_fd_b = uinput_create(&role_b, id, dev_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;
|
||||
}
|
||||
memcpy(v->ui_evdev_b, evdev_b, sizeof v->ui_evdev_b);
|
||||
}
|
||||
|
||||
if (cfg->qmp_path) {
|
||||
@@ -256,19 +278,16 @@ vmctl_t* vmctl_open_uinput_driver(const vmctl_config* cfg) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
+12
-15
@@ -29,12 +29,6 @@ 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++];
|
||||
@@ -65,7 +59,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||
if (rc != 0) return rc; /* not sent = not recorded; never touch the receipt */
|
||||
|
||||
/* Record the actuated key/btn down-bits (write-only; the send path above
|
||||
* never reads this map). abs/rel/scroll have no held state. */
|
||||
* never reads this map). rel/scroll have no held state. */
|
||||
for (int i = 0; i < b->count; i++) {
|
||||
const vmctl_event* e = &b->ev[i];
|
||||
int down = e->value ? 1 : 0;
|
||||
@@ -86,7 +80,7 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* b) {
|
||||
else v->btns_held &= ~mask;
|
||||
break;
|
||||
}
|
||||
default: break; /* abs/rel/scroll: no-op for receipt */
|
||||
default: break; /* rel/scroll: no-op for receipt */
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
@@ -94,13 +88,6 @@ int vmctl_batch_send(vmctl_t* v, vmctl_batch* 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);
|
||||
@@ -154,3 +141,13 @@ unsigned vmctl_btns_snapshot(vmctl_t* v) {
|
||||
if (!v) return 0;
|
||||
return v->btns_held;
|
||||
}
|
||||
|
||||
/* ===== uinput evdev export (UINPUT-only) ===== */
|
||||
|
||||
int vmctl_uinput_evdev(vmctl_t* v, char a[64], char b[64]) {
|
||||
if (!v || v->driver != VMCTL_DRIVER_UINPUT) return -1;
|
||||
int n = 0;
|
||||
if (a) { memcpy(a, v->ui_evdev_a, sizeof v->ui_evdev_a); if (a[0]) n++; }
|
||||
if (b) { memcpy(b, v->ui_evdev_b, sizeof v->ui_evdev_b); if (b[0]) n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,6 @@ static int qmp_driver_send(vmctl_t* v, const vmctl_batch* b) {
|
||||
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}}",
|
||||
@@ -87,7 +82,6 @@ vmctl_t* vmctl_open_qmp_driver(const vmctl_config* cfg) {
|
||||
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;
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
FlatView #0
|
||||
AS "cpu-smm-0", root: mem-container-smram
|
||||
Root memory region: mem-container-smram
|
||||
0000000000000000-0000000000017fff (prio 0, ram): ram0
|
||||
0000000000018000-0000000000018fff (prio 0, ram): synic-0-msg-page
|
||||
000000000001c000-000000007fffffff (prio 0, ram): ram0 @000000000001c000
|
||||
0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3
|
||||
0000000100000000-000000017fffffff (prio 0, ram): ram0 @0000000040000000
|
||||
|
||||
FlatView #1
|
||||
AS "I/O", root: io
|
||||
Root memory region: io
|
||||
0000000000000000-0000000000000007 (prio 0, i/o): dma-chan
|
||||
0000000000000060-0000000000000060 (prio 0, i/o): i8042-data
|
||||
0000000000000064-0000000000000064 (prio 0, i/o): i8042-cmd
|
||||
|
||||
FlatView #2
|
||||
AS "memory", root: system
|
||||
AS "cpu-memory-0", root: system
|
||||
Root memory region: system
|
||||
0000000000000000-0000000000017fff (prio 0, ram): ram0
|
||||
0000000000018000-0000000000018fff (prio 0, ram): synic-0-msg-page
|
||||
0000000000019000-0000000000019fff (prio 0, ram): synic-1-msg-page
|
||||
000000000001a000-000000000001afff (prio 0, ram): synic-2-msg-page
|
||||
000000000001b000-000000000001bfff (prio 0, ram): synic-3-msg-page
|
||||
000000000001c000-000000000002ffff (prio 0, ram): ram0 @000000000001c000
|
||||
0000000000030000-000000000004ffff (prio 1, i/o): smbase-blackhole
|
||||
0000000000050000-00000000000bffff (prio 0, ram): ram0 @0000000000050000
|
||||
00000000000c0000-00000000000dffff (prio 1, rom): pc.rom
|
||||
00000000000e0000-00000000000fffff (prio 0, rom): system.flash0 @000000000035c000
|
||||
0000000000100000-000000007bffffff (prio 0, ram): ram0 @0000000000100000
|
||||
000000007c000000-000000007fffffff (prio 1, i/o): tseg-blackhole
|
||||
0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3
|
||||
0000000082000000-0000000082087fff (prio 0, i/o): vfio-pci-bar0
|
||||
00000000e0000000-00000000efffffff (prio 0, i/o): pcie-mmcfg-mmio
|
||||
00000000fec00000-00000000fec00fff (prio 0, i/o): kvm-ioapic
|
||||
00000000ffc00000-00000000ffc83fff (prio 0, romd): system.flash1
|
||||
0000000100000000-000000027fffffff (prio 0, ram): ram0 @0000000080000000
|
||||
|
||||
FlatView #3
|
||||
AS "pci_bridge_io", root: pci_bridge_io
|
||||
Root memory region: pci_bridge_io
|
||||
@@ -0,0 +1,105 @@
|
||||
/* test_mtree.c — unit tests for mtree_low_split (the below-4G split parser). Pure text in,
|
||||
* number out; no QMP/transport. The fragmented fixture reproduces the structural traps the
|
||||
* old heuristic tripped on (Hyper-V synic overlays, smbase/tseg blackhole holes, rom holes)
|
||||
* plus a decoy non-system flatview that carries its OWN GPA-0 stub and a DIFFERENT @offset,
|
||||
* proving the system address space is selected (not "first match in the text"). */
|
||||
#define _GNU_SOURCE
|
||||
#include "mtree.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef FIXTURE_DIR
|
||||
#define FIXTURE_DIR "."
|
||||
#endif
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { if (!(cond)) { printf(" FAIL: %s\n", (msg)); g_fail = 1; } } while (0)
|
||||
|
||||
/* Slurp a whole text file into a heap buffer (NUL-terminated). NULL on error. */
|
||||
static char* slurp(const char* path) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
||||
long sz = ftell(f);
|
||||
if (sz < 0) { fclose(f); return NULL; }
|
||||
rewind(f);
|
||||
char* buf = malloc((size_t)sz + 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
size_t got = fread(buf, 1, (size_t)sz, f);
|
||||
fclose(f);
|
||||
buf[got] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Re-encode every '\n' as '\r\n' (QEMU's HMP output is CRLF). Caller frees; NULL on OOM. */
|
||||
static char* to_crlf(const char* lf) {
|
||||
size_t n = 0, extra = 0;
|
||||
for (const char* p = lf; *p; p++) { n++; if (*p == '\n') extra++; }
|
||||
char* out = malloc(n + extra + 1);
|
||||
if (!out) return NULL;
|
||||
char* o = out;
|
||||
for (const char* p = lf; *p; p++) { if (*p == '\n') *o++ = '\r'; *o++ = *p; }
|
||||
*o = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Case B: a minimal, NON-fragmented system flatview — one big GPA-0 ram run plus high-RAM
|
||||
* carrying @<low>. Must not be broken by the new parser. */
|
||||
static const char* k_happy =
|
||||
"FlatView #0\n"
|
||||
" AS \"memory\", root: system\n"
|
||||
" Root memory region: system\n"
|
||||
" 0000000000000000-000000007fffffff (prio 0, ram): ram0\n"
|
||||
" 0000000080000000-0000000081ffffff (prio 0, i/o): vfio-pci-bar3\n"
|
||||
" 0000000100000000-000000017fffffff (prio 0, ram): ram0 @0000000080000000\n";
|
||||
|
||||
/* Case C: text without any system flatview => fail-closed. */
|
||||
static const char* k_no_system =
|
||||
"FlatView #0\n"
|
||||
" AS \"I/O\", root: io\n"
|
||||
" Root memory region: io\n"
|
||||
" 0000000000000000-0000000000000007 (prio 0, i/o): dma-chan\n";
|
||||
|
||||
int main(void) {
|
||||
printf("test_mtree\n");
|
||||
|
||||
/* Cases A and E: the fragmented fixture (decoy first, system second). */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof path, "%s/mtree_split_fragmented.txt", FIXTURE_DIR);
|
||||
char* frag = slurp(path);
|
||||
CHECK(frag != NULL, "fragmented fixture loaded");
|
||||
if (frag) {
|
||||
uint64_t low = mtree_low_split(frag);
|
||||
/* A: fragmented low-RAM must NOT yield the GPA-0 stub end (0x18000) — the bug. */
|
||||
CHECK(low == 0x80000000ull, "A: fragmented split == 0x80000000");
|
||||
CHECK(low != 0x18000ull, "A: not the GPA-0 stub end (0x18000)");
|
||||
/* E: the decoy (non-system) flatview comes FIRST and carries @0x40000000; the
|
||||
* function must select the SYSTEM flatview (@0x80000000), not the decoy. */
|
||||
CHECK(low != 0x40000000ull, "E: decoy flatview @offset rejected (system AS chosen)");
|
||||
/* F: real QEMU HMP output is CRLF. The parser MUST tolerate '\r' — a synthetic
|
||||
* LF-only fixture hid this, so the shipped parser returned 0 on the real VM mtree
|
||||
* (-> low=0 -> VM never attached). Regression guard, independent of how git stores
|
||||
* the fixture's line endings. */
|
||||
char* frag_crlf = to_crlf(frag);
|
||||
CHECK(frag_crlf != NULL, "F: CRLF copy allocated");
|
||||
if (frag_crlf) {
|
||||
CHECK(mtree_low_split(frag_crlf) == 0x80000000ull, "F: CRLF fragmented split == 0x80000000");
|
||||
free(frag_crlf);
|
||||
}
|
||||
free(frag);
|
||||
}
|
||||
|
||||
/* Case B: happy path (non-fragmented) still resolves to the high-RAM @offset. */
|
||||
CHECK(mtree_low_split(k_happy) == 0x80000000ull, "B: non-fragmented happy path == 0x80000000");
|
||||
|
||||
/* Case C: no system flatview => 0. */
|
||||
CHECK(mtree_low_split(k_no_system) == 0, "C: no system flatview => fail-closed 0");
|
||||
|
||||
/* Case D: garbage / empty => 0. */
|
||||
CHECK(mtree_low_split("") == 0, "D: empty text => 0");
|
||||
CHECK(mtree_low_split("not an mtree at all\n") == 0, "D: junk text => 0");
|
||||
|
||||
printf("mtree tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* test_uinputlayout.c — DECLARATIVE uinput capability split (pure, no /dev/uinput).
|
||||
*
|
||||
* Verifies the CONSTANT A/B role mapping that drives both device creation and the hot-path
|
||||
* button/wheel carrier selection: device A = keyboard only, device B = relative pointer + buttons
|
||||
* + wheel, and the button/wheel carrier is B. There is no absolute pointer anywhere — the abs role
|
||||
* has been removed and is unrepresentable (no abs field exists in uinput_role). The actuation
|
||||
* ioctls remain armed-only (they need a real /dev/uinput); this covers the layout logic. */
|
||||
#include "vmctl.h"
|
||||
#include "uinput_layout.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { printf(" FAIL: %s\n", (msg)); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
uinput_role a, b; int btn_on_b;
|
||||
|
||||
/* Constant layout: A = keyboard only (no pointer, no buttons/wheel); B = relative pointer +
|
||||
* buttons + wheel; the button/wheel carrier is B. */
|
||||
vmctl_uinput_layout(&a, &b, &btn_on_b);
|
||||
CHECK(a.present && b.present, "two devices");
|
||||
CHECK(a.want_keyboard, "A has keyboard");
|
||||
CHECK(!a.rel_motion, "A has no pointer (keyboard-only)");
|
||||
CHECK(!a.want_buttons, "A has NO mouse buttons");
|
||||
CHECK(!a.want_wheel, "A has NO wheel");
|
||||
CHECK(!b.want_keyboard, "B has no keyboard");
|
||||
CHECK(b.rel_motion, "B is relative");
|
||||
CHECK(b.want_buttons, "B has mouse buttons");
|
||||
CHECK(b.want_wheel, "B has wheel");
|
||||
CHECK(btn_on_b == 1, "button/wheel carrier is B");
|
||||
|
||||
/* No absolute pointer: the abs role is removed and unrepresentable (uinput_role carries no abs
|
||||
* field). The invariant is that each device is either relative or has no pointer at all — A is
|
||||
* keyboard-only (no pointer), B is relative. Neither advertises an absolute axis. */
|
||||
CHECK(!a.rel_motion && !a.want_buttons && !a.want_wheel, "A is keyboard-only (no pointer)");
|
||||
CHECK(b.rel_motion, "B is the relative pointer (not absolute)");
|
||||
|
||||
/* evdev export contract: a NULL handle reports "not a uinput handle" (-1). The populated
|
||||
* path (real /dev/input/eventN) is armed-only — it needs a created uinput device. */
|
||||
{
|
||||
char ea[64], eb[64];
|
||||
CHECK(vmctl_uinput_evdev(NULL, ea, eb) == -1, "evdev export: NULL handle -> -1");
|
||||
}
|
||||
|
||||
printf("uinputlayout tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
+115
-13
@@ -1,7 +1,12 @@
|
||||
/* test_vmhost.c — QEMU/QMP host-plane, armed path: fake QMP server (this test)
|
||||
* <-> real QMP client vmhost. We verify: handshake (greeting -> qmp_capabilities
|
||||
* -> return -> SEAM_UP), async events -> VM_LIFECYCLE (broadcast), CMD_VM{QUERY}
|
||||
* -> command to server -> return -> addressed VM_LIFECYCLE to the initiator, EOF -> SEAM_DOWN. */
|
||||
* -> command to server -> return -> addressed VM_LIFECYCLE to the initiator, EOF -> SEAM_DOWN.
|
||||
*
|
||||
* It also verifies the host->guest input bridge: with bridge_evdev_a/b set in cfg, on reaching
|
||||
* READY the seam adds two input-linux objects (A with grab_all, B without) over its own
|
||||
* connection, with neutral per-endpoint ids and the evdev paths from cfg; the bridge replies
|
||||
* never surface as ACK/VM_LIFECYCLE to control; on teardown it fires object_del for both. */
|
||||
#define _GNU_SOURCE
|
||||
#include "vmsig.h"
|
||||
#include "vmhost.h" /* private cfg (CMake provides the include path) */
|
||||
@@ -25,6 +30,7 @@ static int g_fail = 0;
|
||||
|
||||
static atomic_int g_seamup = 0, g_seamdown = 0;
|
||||
static atomic_int g_paused = 0, g_running_bcast = 0, g_query_reply = 0;
|
||||
static atomic_int g_stray_ack = 0; /* any ACT_ACK reaching control would be a bridge leak */
|
||||
static void* g_ctl = NULL;
|
||||
|
||||
static int on_ev(void* user, const vmsig_event* ev) {
|
||||
@@ -38,6 +44,8 @@ static int on_ev(void* user, const vmsig_event* ev) {
|
||||
vmsig_inproc_send(g_ctl, &d);
|
||||
} else if (ev->kind == VMSIG_EV_SEAM_DOWN && ev->source == VMSIG_SRC_VMHOST) {
|
||||
atomic_store(&g_seamdown, 1);
|
||||
} else if (ev->kind == VMSIG_EV_ACT_ACK && ev->source == VMSIG_SRC_VMHOST) {
|
||||
atomic_fetch_add(&g_stray_ack, 1); /* bridge ops must NOT ack control */
|
||||
} else if (ev->kind == VMSIG_EV_VM_LIFECYCLE) {
|
||||
vmsig_vm_state vs; memcpy(&vs, ev->inln, sizeof vs);
|
||||
if (ev->origin) { /* addressed reply to our QUERY */
|
||||
@@ -64,16 +72,27 @@ static int srv_listen(const char* name) {
|
||||
return fd;
|
||||
}
|
||||
static void srv_send(int fd, const char* s) { ssize_t r = write(fd, s, strlen(s)); (void)r; }
|
||||
|
||||
/* Persistent accumulator: client traffic can interleave (e.g. bridge object_add on READY vs.
|
||||
* CMD_VM query on SEAM_UP), so we keep ALL received bytes and match substrings against the
|
||||
* cumulative text instead of per-call buffers. */
|
||||
static char g_rx[8192];
|
||||
static size_t g_rxlen = 0;
|
||||
static void rx_reset(void) { g_rxlen = 0; g_rx[0] = 0; }
|
||||
static void rx_pump(int fd) {
|
||||
ssize_t r = read(fd, g_rx + g_rxlen, sizeof g_rx - 1 - g_rxlen);
|
||||
if (r > 0) { g_rxlen += (size_t)r; g_rx[g_rxlen] = 0; }
|
||||
}
|
||||
/* Wait until the cumulative client traffic contains needle (or timeout ~2s). */
|
||||
static int srv_expect(int fd, const char* needle) {
|
||||
char buf[1024]; size_t len = 0;
|
||||
for (int i = 0; i < 200; i++) { /* up to ~2s */
|
||||
ssize_t r = read(fd, buf + len, sizeof buf - 1 - len);
|
||||
if (r > 0) { len += (size_t)r; buf[len] = 0; if (strstr(buf, needle)) return 1; }
|
||||
else if (r == 0) return 0;
|
||||
else { struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL); }
|
||||
if (len >= sizeof buf - 1) len = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (strstr(g_rx, needle)) return 1;
|
||||
rx_pump(fd);
|
||||
if (strstr(g_rx, needle)) return 1;
|
||||
struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL);
|
||||
if (g_rxlen >= sizeof g_rx - 1) return strstr(g_rx, needle) != NULL; /* full: stop growing */
|
||||
}
|
||||
return 0;
|
||||
return strstr(g_rx, needle) != NULL;
|
||||
}
|
||||
static void wait_atomic(atomic_int* a, int ms) {
|
||||
for (int i = 0; i < ms; i++) {
|
||||
@@ -98,10 +117,16 @@ int main(void) {
|
||||
g.cap_mask = VMSIG_CAP_OBSERVE | VMSIG_CAP_VM;
|
||||
vmsig_core_add_control(core, vmsig_inproc_control_ops(), ctl, &g);
|
||||
|
||||
/* armed vmhost: it will connect to our fake QMP */
|
||||
vmsig_vmhost_cfg vcfg; memset(&vcfg, 0, sizeof vcfg); vcfg.qmp_path = QMP;
|
||||
/* armed vmhost: it will connect to our fake QMP. Bridge evdev paths set => on READY the
|
||||
* seam should add two input-linux objects forwarding them into the guest. */
|
||||
const char* EVDEV_A = "/dev/input/event42";
|
||||
const char* EVDEV_B = "/dev/input/event43";
|
||||
vmsig_vmhost_cfg vcfg; memset(&vcfg, 0, sizeof vcfg);
|
||||
vcfg.qmp_path = QMP; vcfg.bridge_evdev_a = EVDEV_A; vcfg.bridge_evdev_b = EVDEV_B;
|
||||
CHECK(vmsig_core_add_adapter(core, vmsig_vmhost_ops(), &vcfg, 0) >= 0, "vmhost armed attach");
|
||||
|
||||
rx_reset();
|
||||
|
||||
pthread_t th; pthread_create(&th, NULL, loop_main, core);
|
||||
|
||||
/* === QMP server role === */
|
||||
@@ -113,17 +138,41 @@ int main(void) {
|
||||
|
||||
srv_send(c, "{\"QMP\": {\"version\": {}, \"capabilities\": []}}\r\n");
|
||||
CHECK(srv_expect(c, "qmp_capabilities"), "client sent qmp_capabilities");
|
||||
srv_send(c, "{\"return\": {}}\r\n"); /* -> READY -> SEAM_UP */
|
||||
srv_send(c, "{\"return\": {}}\r\n"); /* -> READY -> SEAM_UP + bridge */
|
||||
|
||||
/* On READY the seam adds the input-linux bridge BEFORE the SEAM_UP-driven CMD_VM query
|
||||
* (bridge ids 1,2; query id 3). Verify both object_add lines and their properties. */
|
||||
CHECK(srv_expect(c, "object-add"), "seam sent object-add (input-linux bridge)");
|
||||
CHECK(srv_expect(c, "\"input-linux\""), "bridge object uses qom-type input-linux");
|
||||
CHECK(srv_expect(c, "\"vmsig-in-a-0\""), "bridge A has neutral per-endpoint id");
|
||||
CHECK(srv_expect(c, "\"vmsig-in-b-0\""), "bridge B has neutral per-endpoint id");
|
||||
CHECK(srv_expect(c, EVDEV_A), "bridge A carries the cfg evdev path for A");
|
||||
CHECK(srv_expect(c, EVDEV_B), "bridge B carries the cfg evdev path for B");
|
||||
srv_send(c, "{\"return\": {}, \"id\": 1}\r\n"); /* ack bridge A (consumed silently) */
|
||||
srv_send(c, "{\"return\": {}, \"id\": 2}\r\n"); /* ack bridge B (consumed silently) */
|
||||
|
||||
/* grab_all must be on A only: it appears exactly once, and only A's add carries it. The
|
||||
* accumulator holds A's line ending in grab_all before B's id; assert B's add has none
|
||||
* by checking grab_all precedes "vmsig-in-b-0" in the stream and never reappears. */
|
||||
{
|
||||
const char* g1 = strstr(g_rx, "grab_all");
|
||||
const char* g2 = g1 ? strstr(g1 + 1, "grab_all") : NULL;
|
||||
const char* bid = strstr(g_rx, "vmsig-in-b-0");
|
||||
CHECK(g1 != NULL, "grab_all present (on A)");
|
||||
CHECK(g2 == NULL, "grab_all appears exactly once (A only)");
|
||||
CHECK(g1 && bid && g1 < bid, "grab_all belongs to A's add, not B's");
|
||||
}
|
||||
|
||||
srv_send(c, "{\"event\": \"STOP\"}\r\n"); /* -> broadcast PAUSED */
|
||||
CHECK(srv_expect(c, "query-status"), "client sent query-status (from CMD_VM)");
|
||||
srv_send(c, "{\"return\": {\"status\": \"running\"}, \"id\": 1}\r\n"); /* -> addressed reply */
|
||||
srv_send(c, "{\"return\": {\"status\": \"running\"}, \"id\": 3}\r\n"); /* -> addressed reply */
|
||||
srv_send(c, "{\"event\": \"RESUME\"}\r\n"); /* -> broadcast RUNNING */
|
||||
|
||||
wait_atomic(&g_seamup, 1000);
|
||||
wait_atomic(&g_paused, 1000);
|
||||
wait_atomic(&g_query_reply, 1000);
|
||||
wait_atomic(&g_running_bcast, 1000);
|
||||
CHECK(atomic_load(&g_stray_ack) == 0, "bridge ops did not leak ACK to control");
|
||||
|
||||
close(c); /* EOF -> SEAM_DOWN */
|
||||
wait_atomic(&g_seamdown, 1000);
|
||||
@@ -139,6 +188,59 @@ int main(void) {
|
||||
pthread_join(th, NULL);
|
||||
vmsig_core_free(core);
|
||||
vmsig_ctx_free(ctx);
|
||||
|
||||
/* === Scenario 2: object_del on a clean reap (connection still READY) ===
|
||||
* The EOF path above marks the seam DEAD, so its best-effort del is (correctly) skipped.
|
||||
* Here we reach READY then free the core WITHOUT EOF: vh_close must fire object_del for
|
||||
* both bridge ids over the still-open socket before closing its fd. */
|
||||
{
|
||||
const char* QMP2 = "@vmsig-qmp-fake-test-2";
|
||||
int srv2 = srv_listen(QMP2);
|
||||
CHECK(srv2 >= 0, "scenario2: srv_listen");
|
||||
if (srv2 >= 0) {
|
||||
vmsig_ctx* ctx2 = vmsig_ctx_new();
|
||||
vmsig_core* core2 = vmsig_core_new(ctx2);
|
||||
vmsig_vmhost_cfg vc2; memset(&vc2, 0, sizeof vc2);
|
||||
vc2.qmp_path = QMP2; vc2.bridge_evdev_a = EVDEV_A; vc2.bridge_evdev_b = EVDEV_B;
|
||||
CHECK(vmsig_core_add_adapter(core2, vmsig_vmhost_ops(), &vc2, 0) >= 0,
|
||||
"scenario2: vmhost attach");
|
||||
rx_reset();
|
||||
pthread_t th2; pthread_create(&th2, NULL, loop_main, core2);
|
||||
|
||||
int c2 = accept(srv2, NULL, NULL);
|
||||
CHECK(c2 >= 0, "scenario2: accept");
|
||||
if (c2 >= 0) {
|
||||
struct timeval tv2 = { 0, 50 * 1000 };
|
||||
setsockopt(c2, SOL_SOCKET, SO_RCVTIMEO, &tv2, sizeof tv2);
|
||||
srv_send(c2, "{\"QMP\": {\"version\": {}, \"capabilities\": []}}\r\n");
|
||||
CHECK(srv_expect(c2, "qmp_capabilities"), "scenario2: qmp_capabilities");
|
||||
srv_send(c2, "{\"return\": {}}\r\n"); /* READY -> bridge add */
|
||||
CHECK(srv_expect(c2, "vmsig-in-b-0"), "scenario2: bridge added");
|
||||
srv_send(c2, "{\"return\": {}, \"id\": 1}\r\n");
|
||||
srv_send(c2, "{\"return\": {}, \"id\": 2}\r\n");
|
||||
|
||||
/* Clean reap WITHOUT EOF: stop the loop then free (vh_close fires del). */
|
||||
vmsig_core_stop(core2);
|
||||
pthread_join(th2, NULL);
|
||||
vmsig_core_free(core2);
|
||||
|
||||
for (int i = 0; i < 50 && !strstr(g_rx, "object-del"); i++) {
|
||||
rx_pump(c2);
|
||||
struct timespec t = { 0, 10 * 1000000 }; nanosleep(&t, NULL);
|
||||
}
|
||||
const char* d = strstr(g_rx, "object-del");
|
||||
CHECK(d != NULL, "scenario2: teardown fired object-del");
|
||||
CHECK(strstr(g_rx, "vmsig-in-a-0"), "scenario2: object_del for bridge A");
|
||||
CHECK(strstr(g_rx, "vmsig-in-b-0"), "scenario2: object_del for bridge B");
|
||||
close(c2);
|
||||
} else {
|
||||
vmsig_core_stop(core2); pthread_join(th2, NULL); vmsig_core_free(core2);
|
||||
}
|
||||
vmsig_ctx_free(ctx2);
|
||||
close(srv2);
|
||||
}
|
||||
}
|
||||
|
||||
close(srv);
|
||||
|
||||
printf("vmhost tests: %s\n", g_fail ? "FAIL" : "PASS");
|
||||
|
||||
Reference in New Issue
Block a user