fix(input): re-add the input-linux bridge object idempotently

A bare object-add fails with "duplicate property '<id>'" when a prior daemon
instance left the object live: its best-effort object-del on teardown has no
round-trip and can race a fast restart, so device B (the relative mouse)
stays orphaned and motion is not forwarded.

Fire object-del for the same id before each object-add. QMP is sequential
per connection, so the del is applied before the add; on a clean first attach
it no-ops (DeviceNotFound, silently dropped). Re-attach is now idempotent.
This commit is contained in:
2026-06-24 22:01:12 +03:00
parent 0f452fe37c
commit 7ab6119b1f
2 changed files with 27 additions and 5 deletions
+15 -3
View File
@@ -121,6 +121,9 @@ static void bridge_id(char* out, size_t cap, uint32_t ep, char ab) {
snprintf(out, cap, "vmsig-in-%c-%u", ab, ep);
}
/* Best-effort object-del of a possibly-stale object id (defined below; fwd for bridge_add). */
static void bridge_del_fire(struct vmsig_adapter* a, char ab);
/* 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
@@ -130,6 +133,13 @@ static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int g
if (!p) return -1;
char id[32];
bridge_id(id, sizeof id, a->endpoint, ab);
/* Idempotent re-attach: fire object-del for this id FIRST. A prior daemon instance tears the
* bridge down best-effort WITHOUT a round-trip (bridge_del_fire in vh_close), and a fast
* restart/redeploy can reach here before QEMU processed that del — leaving the object live,
* so a bare object-add fails with "duplicate property '<id>'" (observed for device B). QMP is
* sequential per connection, so this del is applied before the add below; on a clean first
* attach it just no-ops (DeviceNotFound, silently dropped — that frame carries no QMP id). */
bridge_del_fire(a, ab);
uint32_t qid = ++a->next_id;
char line[320];
int len = snprintf(line, sizeof line,
@@ -144,9 +154,11 @@ static int bridge_add(struct vmsig_adapter* a, char ab, const char* evdev, int g
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). */
/* Best-effort object-del, no reply awaited. Fired in TWO places: on teardown before the fd
* closes (vh_close) AND before every object-add (bridge_add, idempotent re-attach). QEMU drops
* these objects when the VM powers off, so del matters on a detach/re-attach without power-off
* (daemon restart / endpoint move). A del of an absent id no-ops (DeviceNotFound, silently
* dropped — this frame carries no QMP id, so handle_line finds no pend). */
static void bridge_del_fire(struct vmsig_adapter* a, char ab) {
char id[32];
bridge_id(id, sizeof id, a->endpoint, ab);
+12 -2
View File
@@ -5,8 +5,10 @@
*
* 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. */
* connection, with neutral per-endpoint ids and the evdev paths from cfg; each add is preceded
* by an idempotent object-del of the same id (clears a stale object from a crashed/racing prior
* daemon); 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) */
@@ -148,6 +150,10 @@ int main(void) {
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");
/* Idempotent re-attach: each add is preceded by an object-del of the same id. The EOF
* teardown below skips del (seam DEAD), so this object-del can ONLY originate from the
* del-before-add path. */
CHECK(srv_expect(c, "object-del"), "bridge fires object-del before add (idempotent re-attach)");
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) */
@@ -219,6 +225,10 @@ int main(void) {
srv_send(c2, "{\"return\": {}, \"id\": 1}\r\n");
srv_send(c2, "{\"return\": {}, \"id\": 2}\r\n");
/* Attach already emitted object-del (del-before-add). Reset the accumulator so the
* teardown del below is verified in ISOLATION, not satisfied by the attach del. */
rx_reset();
/* Clean reap WITHOUT EOF: stop the loop then free (vh_close fires del). */
vmsig_core_stop(core2);
pthread_join(th2, NULL);