From nobody Sat Apr 11 17:09:10 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1775719203; cv=none; d=zohomail.com; s=zohoarc; b=T2QNPTSiM18vk06gjPPEy2WER4Y14EGoA+0FmaWt2Fdf7zytBYbu0qvqsDHYi54egtvfVZuX95iUTveqQ6bv39M6c4z7yacc0OEaZfNAt/xuiuEIl71BzQtDzvtgOJfHhFJiNszqxgPff/4FPIJJS9n8whvYEi9oO7M5Gz9erbU= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775719203; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=a5uZGIlriM4UJ2YCjCXNpAnlM2tGtC747ss/XcQktXE=; b=MRn/h+YO8ZeMqNSgk4VUCgR7RdqdKoyAKAQyfAxUHtyartz3zsi0c9lh8ljWi1Nv2eBHy67895fmamaSbOQIWgZM4BmxUAcU6UYgC13gMIipaF3UAPHz9DqUmMwgW8kY+N7b0sIyYJHVYj6M421JoaXghGEj0F/XD+H64XXSDE4= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775719203460832.0946515748791; Thu, 9 Apr 2026 00:20:03 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wAjf8-0007LC-9d; Thu, 09 Apr 2026 03:18:50 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf3-0007KR-F5 for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:45 -0400 Received: from sea.source.kernel.org ([172.234.252.31]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf1-0000Yq-GH for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:45 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by sea.source.kernel.org (Postfix) with ESMTP id 013D943A90; Thu, 9 Apr 2026 07:18:35 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 97F9FC4CEF7; Thu, 9 Apr 2026 07:18:32 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775719114; bh=UUlm8e38M0cU+sF8CtorJzdeLsTyxHdaQbX9fCniErs=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=W95FoXUCMXa1JBrkxNP6NTKMLmsElXzScxBksdQixMLBO2vMLudtmpI3huMkdienc YRrk6m6pv2n/9jAO+qREHxaj/0IeF3NaAA9Dp0svnqFjvmikoxD/jtiG0Os/8jOmYv YmuoK4LZf0VwxgkNwp7y/PD6Um1hCoR7S7cv6NCrtGonM27wlTVBW85G76whvgycjO 7oIw4JGs+oPQJ7Od15rPRvUotPPfoBcUIN66VQX9hHahHpW/dd0vRGmAp4wEpRj8Z8 pdvfs8IqVDe7vuRDKCqtvRIVSPDLhq35J7YzAHgsnI36xXMDUZEJr0ES8FoaRRJe7/ aVEC65BMsdPdw== From: Christian Brauner Date: Thu, 09 Apr 2026 09:18:18 +0200 Subject: [PATCH v4 1/5] monitor: store monitor id in Monitor struct MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260409-work-qmp-monitor-hotplug-v4-1-89c4fdf69df1@kernel.org> References: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> In-Reply-To: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> To: qemu-devel@nongnu.org Cc: Markus Armbruster , Eric Blake , Fabiano Rosas , Laurent Vivier , Paolo Bonzini , Thomas Huth , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Christian Brauner X-Mailer: b4 0.16-dev X-Developer-Signature: v=1; a=openpgp-sha256; l=7163; i=brauner@kernel.org; h=from:subject:message-id; bh=UUlm8e38M0cU+sF8CtorJzdeLsTyxHdaQbX9fCniErs=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWReDzpq7DWn5f3W3yXX3G5ecmOcuMjP8N9CZR/pxVVn+ mUnplov6ShlYRDjYpAVU2RxaDcJl1vOU7HZKFMDZg4rE8gQBi5OAZjIVl2G/zWLE10+bF56armg e7mVpOLPPVd5FKNOLa52UZ77p1qf3ZPhv5PMRZEk+UsK64SYxM3FNslG8lZvjOKSf9znsW/xs8M eHAA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=172.234.252.31; envelope-from=brauner@kernel.org; helo=sea.source.kernel.org X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @kernel.org) X-ZM-MESSAGEID: 1775719205643158500 Add an 'id' field to struct Monitor. The id field stores the monitor identifier from MonitorOptions which was previously parsed but discarded. Auto-generate a unique id ("mon0", "mon1", ...) for QMP monitors created via CLI without an explicit id, so that every QMP monitor is addressable by monitor-remove and always appears with an id in query-monitors output. Extend monitor_init_qmp() to accept an id parameter so it is set before the monitor is added to mon_list. For iothread monitors, move monitor_list_append() from the setup BH to the caller so monitor_find_by_id() can detect duplicates immediately. Without this, two concurrent monitor-add calls could both pass the duplicate check before either BH runs. This means the monitor is now visible in mon_list before its chardev handlers are set up, which was not the case before. This is safe because the request queue is still empty (no chardev handlers means no monitor_qmp_read(), so the dispatcher finds nothing to dispatch) and event broadcast is handled below. This requires initializing mon->commands =3D &qmp_cap_negotiation_commands before monitor_list_append(). Without it, commands is NULL (from g_new0) and monitor_qapi_event_emit() would not skip the monitor during event broadcast -- its check is specifically for the qmp_cap_negotiation_commands pointer, so a NULL falls through to qmp_send_response() on an uninitialized monitor. CHR_EVENT_OPENED sets commands to the same value again later. Add monitor_find_by_id() to look up monitors by identifier. The lookup takes monitor_lock to serialize with the I/O thread BH that modifies mon_list, but releases it before returning. The caller must hold the BQL to ensure the returned pointer remains valid since only BQL holders can destroy monitors. Free the id string in monitor_data_destroy(). Signed-off-by: Christian Brauner (Amutable) --- include/monitor/monitor.h | 3 ++- monitor/monitor-internal.h | 4 +++- monitor/monitor.c | 21 ++++++++++++++++++++- monitor/qmp.c | 15 ++++++++++++--- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/include/monitor/monitor.h b/include/monitor/monitor.h index 296690e1f1..a4e6aaa97f 100644 --- a/include/monitor/monitor.h +++ b/include/monitor/monitor.h @@ -19,7 +19,8 @@ bool monitor_cur_is_qmp(void); =20 void monitor_init_globals(void); void monitor_init_globals_core(void); -void monitor_init_qmp(Chardev *chr, bool pretty, Error **errp); +void monitor_init_qmp(Chardev *chr, bool pretty, const char *id, + Error **errp); void monitor_init_hmp(Chardev *chr, bool use_readline, Error **errp); int monitor_init(MonitorOptions *opts, bool allow_hmp, Error **errp); int monitor_init_opts(QemuOpts *opts, Error **errp); diff --git a/monitor/monitor-internal.h b/monitor/monitor-internal.h index feca111ae3..24d3b1900e 100644 --- a/monitor/monitor-internal.h +++ b/monitor/monitor-internal.h @@ -98,7 +98,7 @@ struct Monitor { bool is_qmp; bool skip_flush; bool use_io_thread; - + char *id; char *mon_cpu_path; QTAILQ_ENTRY(Monitor) entry; =20 @@ -181,6 +181,8 @@ void monitor_data_destroy_qmp(MonitorQMP *mon); void coroutine_fn monitor_qmp_dispatcher_co(void *data); void qmp_dispatcher_co_wake(void); =20 +Monitor *monitor_find_by_id(const char *id); + int get_monitor_def(Monitor *mon, int64_t *pval, const char *name); void handle_hmp_command(MonitorHMP *mon, const char *cmdline); int hmp_compare_cmd(const char *name, const char *list); diff --git a/monitor/monitor.c b/monitor/monitor.c index 00b93ed612..10a32150e9 100644 --- a/monitor/monitor.c +++ b/monitor/monitor.c @@ -622,6 +622,7 @@ void monitor_data_init(Monitor *mon, bool is_qmp, bool = skip_flush, =20 void monitor_data_destroy(Monitor *mon) { + g_free(mon->id); g_free(mon->mon_cpu_path); qemu_chr_fe_deinit(&mon->chr, false); if (monitor_is_qmp(mon)) { @@ -633,6 +634,24 @@ void monitor_data_destroy(Monitor *mon) qemu_mutex_destroy(&mon->mon_lock); } =20 +/* + * Look up a monitor by its id. The monitor_lock is released before + * returning, so the caller must hold the BQL to ensure the returned + * pointer remains valid (only BQL holders can destroy monitors). + */ +Monitor *monitor_find_by_id(const char *id) +{ + Monitor *mon; + + QEMU_LOCK_GUARD(&monitor_lock); + QTAILQ_FOREACH(mon, &mon_list, entry) { + if (mon->id && strcmp(mon->id, id) =3D=3D 0) { + return mon; + } + } + return NULL; +} + void monitor_cleanup(void) { /* @@ -732,7 +751,7 @@ int monitor_init(MonitorOptions *opts, bool allow_hmp, = Error **errp) =20 switch (opts->mode) { case MONITOR_MODE_CONTROL: - monitor_init_qmp(chr, opts->pretty, errp); + monitor_init_qmp(chr, opts->pretty, opts->id, errp); break; case MONITOR_MODE_READLINE: if (!allow_hmp) { diff --git a/monitor/qmp.c b/monitor/qmp.c index 687019811f..bba69a3a40 100644 --- a/monitor/qmp.c +++ b/monitor/qmp.c @@ -510,10 +510,10 @@ static void monitor_qmp_setup_handlers_bh(void *opaqu= e) qemu_chr_fe_set_handlers(&mon->common.chr, monitor_can_read, monitor_qmp_read, monitor_qmp_event, NULL, &mon->common, context, true); - monitor_list_append(&mon->common); } =20 -void monitor_init_qmp(Chardev *chr, bool pretty, Error **errp) +void monitor_init_qmp(Chardev *chr, bool pretty, const char *id, + Error **errp) { MonitorQMP *mon =3D g_new0(MonitorQMP, 1); =20 @@ -527,12 +527,20 @@ void monitor_init_qmp(Chardev *chr, bool pretty, Erro= r **errp) monitor_data_init(&mon->common, true, false, qemu_chr_has_feature(chr, QEMU_CHAR_FEATURE_GCONTEXT= )); =20 + if (id) { + mon->common.id =3D g_strdup(id); + } else { + static unsigned int qmp_monitor_id_counter; + mon->common.id =3D g_strdup_printf("mon%u", qmp_monitor_id_counter= ++); + } mon->pretty =3D pretty; =20 qemu_mutex_init(&mon->qmp_queue_lock); mon->qmp_requests =3D g_queue_new(); =20 json_message_parser_init(&mon->parser, handle_qmp_command, mon, NULL); + /* Prevent event broadcast to an uninitialized monitor. */ + mon->commands =3D &qmp_cap_negotiation_commands; if (mon->common.use_io_thread) { /* * Make sure the old iowatch is gone. It's possible when @@ -551,7 +559,8 @@ void monitor_init_qmp(Chardev *chr, bool pretty, Error = **errp) */ aio_bh_schedule_oneshot(iothread_get_aio_context(mon_iothread), monitor_qmp_setup_handlers_bh, mon); - /* The bottom half will add @mon to @mon_list */ + /* Synchronous insert for immediate duplicate detection. */ + monitor_list_append(&mon->common); } else { qemu_chr_fe_set_handlers(&mon->common.chr, monitor_can_read, monitor_qmp_read, monitor_qmp_event, --=20 2.47.3 From nobody Sat Apr 11 17:09:10 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1775719203; cv=none; d=zohomail.com; s=zohoarc; b=Alqn4l5uXYdIbANxDgiZ89jW0xsF8YOUFbCeL62wtPxwLrd+yTbnjDVU158qboRJFw74465AzY8uMz8FflmuuF61iCX/ppkiXabpnYxtY/1nP8HKlUyUGJU3kCU2RcA+8tOAlsOWJMaBlcUkxC/t0SOIUpBQCFHLwYsAjifVaxU= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775719203; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=2xMSsHhKwavqc12Fa+WjgG06Y+90+HzWCFrbentdtJ8=; b=CIuQdxFLf15aC7k87OKNnt88IzUNK0kgSAOHhQz/igreAnMP0hiVUttjjpDjgDNhVNgv3REaWI5NzgnZW/vI8fU7OufsGodaGuChwf4JMSa4XjTd+TMEKs73U+RYeRftN2QbYGixsXiPUPdUgmTuwnT0ZH3VEOJRHwijDu+cbfM= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775719203578567.3725948607539; Thu, 9 Apr 2026 00:20:03 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wAjfC-0007Ly-Gy; Thu, 09 Apr 2026 03:18:56 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf8-0007LF-BD for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:50 -0400 Received: from tor.source.kernel.org ([2600:3c04:e001:324:0:1991:8:25]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf3-0000Z1-Cl for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:47 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id CEF28600CB; Thu, 9 Apr 2026 07:18:37 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 56B30C116C6; Thu, 9 Apr 2026 07:18:35 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775719117; bh=mLUvBPDhccnFM02xf0pWfI0UrLpviv0orpFNSVCxFcE=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=rHoakGGNosZVdZXkvGCBifeU8JgSaj6uh/h2KoPc7qZUHx48Ob1k7TXU9+sS8HW/5 6DFEyuzYs7XvSwqO08wMxsNMDpmJxgRRCPJWr4wHeyJV7iuwDw5YmZlBOATm4EAGrP x4hSkl33tOVWCFjI4GfKvO2UVHtbwNUh5ThmPCofXbxoa3mFuPiQgLHfk0nbVRM7iN 5r9RMnutjs9f7kCR8Z0oFLPZoBhOah305snwjE1tlnKBR1M7gCweoKSV2yUYlWO8u2 aYJxGT8Q+rgAwaJckEGiIOVQKhlUBmo/aUvClGdjxeTGGFTF0RDLLB6010NSR9dp86 HD6WXA0Dyk+uA== From: Christian Brauner Date: Thu, 09 Apr 2026 09:18:19 +0200 Subject: [PATCH v4 2/5] monitor/qmp: add infrastructure for safe dynamic monitor removal MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260409-work-qmp-monitor-hotplug-v4-2-89c4fdf69df1@kernel.org> References: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> In-Reply-To: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> To: qemu-devel@nongnu.org Cc: Markus Armbruster , Eric Blake , Fabiano Rosas , Laurent Vivier , Paolo Bonzini , Thomas Huth , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Christian Brauner X-Mailer: b4 0.16-dev X-Developer-Signature: v=1; a=openpgp-sha256; l=13134; i=brauner@kernel.org; h=from:subject:message-id; bh=mLUvBPDhccnFM02xf0pWfI0UrLpviv0orpFNSVCxFcE=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWReDzr61GvfnwOTWaqm27mXc51VnNnv2RsoyC+oe8v+x eSLDmebO0pZGMS4GGTFFFkc2k3C5ZbzVGw2ytSAmcPKBDKEgYtTACbyWJfhf2b6//kHmpd+vi6p 1+J43uHas4SQuF8dgeemmE/jTrnGqcDwV/hOoI+b1pcLVQe7Q9sN73kIPnNemn7PR3N2Kd9dFfk MFgA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2600:3c04:e001:324:0:1991:8:25; envelope-from=brauner@kernel.org; helo=tor.source.kernel.org X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @kernel.org) X-ZM-MESSAGEID: 1775719205687158500 Add 'dead' field to struct Monitor for deferred destruction during monitor-remove. Add monitor_qmp_destroy() to allow destroying a single QMP monitor at runtime without shutting down the entire dispatcher coroutine. Convert monitor_accept_input from a oneshot BH (aio_bh_schedule_oneshot) to a persistent BH (aio_bh_new + qemu_bh_schedule). Oneshot BHs cannot be cancelled, so monitor_resume() racing with destruction would schedule a callback against memory that monitor_qmp_destroy() is about to free. A persistent BH can be deleted during destruction, cancelling any pending schedule. Move qemu_chr_fe_accept_input() under mon_lock in monitor_accept_input() so it cannot race with monitor_qmp_destroy() which deletes the BH under the same lock. Extract monitor_cancel_out_watch() for cancelling a pending out_watch GSource. g_source_remove() only searches the default GMainContext but iothread monitors attach the watch to the iothread context, so g_main_context_find_source_by_id() with the correct context followed by g_source_destroy() is needed. When chardev handlers have been disconnected via qemu_chr_fe_set_handlers(NULL), s->gcontext is reset to NULL by qemu_chr_be_update_read_handlers(). A watch created after the reset (e.g. by a self-removal response flush) lands on the default GMainContext rather than the iothread context. Fall back to searching the default context when the iothread context search misses. The monitor_qmp_destroy() sequence is: 1. Delete accept_input_bh (cancel pending resume). 2. Under mon_lock: set skip_flush so no further writes can create new out_watch GSources, then cancel any existing out_watch. skip_flush must be set first because the chardev gcontext has already been reset to NULL by handler disconnect -- a flush at this point would attach the watch to the default GMainContext rather than the iothread context, and monitor_cancel_out_watch() searching the iothread context would miss it, leaking a GSource that fires monitor_unblocked() on freed memory. 3. For iothread monitors: synchronize with the iothread via aio_wait_bh_oneshot(). An in-flight monitor_unblocked() GSource callback may be blocked on mon_lock (held in step 2) and will resume after we release it. Since BHs cannot fire while a GSource callback is dispatching, the no-op BH only runs after monitor_unblocked() has returned, making destruction safe. 4. Final drain of the request queue to catch requests enqueued by an in-flight monitor_qmp_read() that raced with the drain in qmp_monitor_remove(). 5. monitor_data_destroy() + g_free(). Add qmp_dispatcher_current_mon tracking in the dispatcher coroutine to handle self-removal (a monitor sends monitor-remove targeting itself). Both the dispatcher yield points and QMP command handlers run under the BQL, so no additional locking is needed. After dispatching each request, the dispatcher checks the dead flag: if set, it closes the chardev connection, calls monitor_qmp_destroy(), and clears the tracking pointer. monitor_resume() is skipped because the chardev handlers are already disconnected, and resume would schedule a BH against a monitor about to be freed. Add a setup_pending flag for iothread monitors so qmp_monitor_remove() can reject removal before the setup BH has completed. Signed-off-by: Christian Brauner (Amutable) --- monitor/monitor-internal.h | 8 ++++++ monitor/monitor.c | 49 ++++++++++++++++++++++++++-------- monitor/qmp.c | 66 ++++++++++++++++++++++++++++++++++++++++++= ++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/monitor/monitor-internal.h b/monitor/monitor-internal.h index 24d3b1900e..f655ac5611 100644 --- a/monitor/monitor-internal.h +++ b/monitor/monitor-internal.h @@ -98,6 +98,9 @@ struct Monitor { bool is_qmp; bool skip_flush; bool use_io_thread; + bool dead; /* awaiting drain after monitor-remove */ + QEMUBH *accept_input_bh; /* persistent BH for monitor_accept_input = */ + char *id; char *mon_cpu_path; QTAILQ_ENTRY(Monitor) entry; @@ -135,6 +138,7 @@ typedef struct { Monitor common; JSONMessageParser parser; bool pretty; + bool setup_pending; /* iothread BH has not yet set up chardev handler= s */ /* * When a client connects, we're in capabilities negotiation mode. * @commands is &qmp_cap_negotiation_commands then. When command @@ -173,15 +177,19 @@ void monitor_data_init(Monitor *mon, bool is_qmp, boo= l skip_flush, bool use_io_thread); void monitor_data_destroy(Monitor *mon); int monitor_can_read(void *opaque); +void monitor_cancel_out_watch(Monitor *mon); void monitor_list_append(Monitor *mon); void monitor_fdsets_cleanup(void); =20 void qmp_send_response(MonitorQMP *mon, const QDict *rsp); void monitor_data_destroy_qmp(MonitorQMP *mon); +void monitor_qmp_destroy(MonitorQMP *mon); +void monitor_qmp_drain_queue(MonitorQMP *mon); void coroutine_fn monitor_qmp_dispatcher_co(void *data); void qmp_dispatcher_co_wake(void); =20 Monitor *monitor_find_by_id(const char *id); +bool monitor_qmp_dispatcher_is_servicing(MonitorQMP *mon); =20 int get_monitor_def(Monitor *mon, int64_t *pval, const char *name); void handle_hmp_command(MonitorHMP *mon, const char *cmdline); diff --git a/monitor/monitor.c b/monitor/monitor.c index 10a32150e9..2bf0a4934f 100644 --- a/monitor/monitor.c +++ b/monitor/monitor.c @@ -146,6 +146,28 @@ static gboolean monitor_unblocked(void *do_not_use, GI= OCondition cond, return G_SOURCE_REMOVE; } =20 +/* Cancel a pending out_watch GSource. Caller must hold mon_lock. */ +void monitor_cancel_out_watch(Monitor *mon) +{ + if (mon->out_watch) { + GMainContext *ctx =3D NULL; + GSource *src; + + if (mon->use_io_thread) { + ctx =3D iothread_get_g_main_context(mon_iothread); + } + src =3D g_main_context_find_source_by_id(ctx, mon->out_watch); + if (!src && ctx) { + /* Handler disconnect may have reset gcontext to NULL. */ + src =3D g_main_context_find_source_by_id(NULL, mon->out_watch); + } + if (src) { + g_source_destroy(src); + } + mon->out_watch =3D 0; + } +} + /* Caller must hold mon->mon_lock */ void monitor_flush_locked(Monitor *mon) { @@ -545,13 +567,13 @@ static void monitor_accept_input(void *opaque) MonitorHMP *hmp_mon =3D container_of(mon, MonitorHMP, common); assert(hmp_mon->rs); readline_restart(hmp_mon->rs); + qemu_chr_fe_accept_input(&mon->chr); qemu_mutex_unlock(&mon->mon_lock); readline_show_prompt(hmp_mon->rs); } else { + qemu_chr_fe_accept_input(&mon->chr); qemu_mutex_unlock(&mon->mon_lock); } - - qemu_chr_fe_accept_input(&mon->chr); } =20 void monitor_resume(Monitor *mon) @@ -561,15 +583,7 @@ void monitor_resume(Monitor *mon) } =20 if (qatomic_dec_fetch(&mon->suspend_cnt) =3D=3D 0) { - AioContext *ctx; - - if (mon->use_io_thread) { - ctx =3D iothread_get_aio_context(mon_iothread); - } else { - ctx =3D qemu_get_aio_context(); - } - - aio_bh_schedule_oneshot(ctx, monitor_accept_input, mon); + qemu_bh_schedule(mon->accept_input_bh); } =20 trace_monitor_suspend(mon, -1); @@ -610,6 +624,8 @@ static void monitor_iothread_init(void) void monitor_data_init(Monitor *mon, bool is_qmp, bool skip_flush, bool use_io_thread) { + AioContext *ctx; + if (use_io_thread && !mon_iothread) { monitor_iothread_init(); } @@ -618,6 +634,13 @@ void monitor_data_init(Monitor *mon, bool is_qmp, bool= skip_flush, mon->outbuf =3D g_string_new(NULL); mon->skip_flush =3D skip_flush; mon->use_io_thread =3D use_io_thread; + + if (use_io_thread) { + ctx =3D iothread_get_aio_context(mon_iothread); + } else { + ctx =3D qemu_get_aio_context(); + } + mon->accept_input_bh =3D aio_bh_new(ctx, monitor_accept_input, mon); } =20 void monitor_data_destroy(Monitor *mon) @@ -631,6 +654,10 @@ void monitor_data_destroy(Monitor *mon) readline_free(container_of(mon, MonitorHMP, common)->rs); } g_string_free(mon->outbuf, true); + if (mon->accept_input_bh) { + qemu_bh_delete(mon->accept_input_bh); + mon->accept_input_bh =3D NULL; + } qemu_mutex_destroy(&mon->mon_lock); } =20 diff --git a/monitor/qmp.c b/monitor/qmp.c index bba69a3a40..a56f7240e0 100644 --- a/monitor/qmp.c +++ b/monitor/qmp.c @@ -26,6 +26,7 @@ =20 #include "chardev/char-io.h" #include "monitor-internal.h" +#include "qemu/aio-wait.h" #include "qapi/error.h" #include "qapi/qapi-commands-control.h" #include "qobject/qdict.h" @@ -71,6 +72,9 @@ typedef struct QMPRequest QMPRequest; =20 QmpCommandList qmp_commands, qmp_cap_negotiation_commands; =20 +/* Monitor being serviced by the dispatcher. Protected by BQL. */ +static MonitorQMP *qmp_dispatcher_current_mon; + static bool qmp_oob_enabled(MonitorQMP *mon) { return mon->capab[QMP_CAPABILITY_OOB]; @@ -98,6 +102,12 @@ static void monitor_qmp_cleanup_req_queue_locked(Monito= rQMP *mon) } } =20 +void monitor_qmp_drain_queue(MonitorQMP *mon) +{ + QEMU_LOCK_GUARD(&mon->qmp_queue_lock); + monitor_qmp_cleanup_req_queue_locked(mon); +} + static void monitor_qmp_cleanup_queue_and_resume(MonitorQMP *mon) { QEMU_LOCK_GUARD(&mon->qmp_queue_lock); @@ -287,6 +297,7 @@ void coroutine_fn monitor_qmp_dispatcher_co(void *data) */ =20 mon =3D req_obj->mon; + qmp_dispatcher_current_mon =3D mon; =20 /* * We need to resume the monitor if handle_qmp_command() @@ -342,11 +353,26 @@ void coroutine_fn monitor_qmp_dispatcher_co(void *dat= a) qobject_unref(rsp); } =20 + /* + * Self-removal: monitor-remove marked this monitor dead. + * Close chardev, destroy, skip monitor_resume(). + */ + if (mon->common.dead) { + qemu_chr_fe_set_handlers(&mon->common.chr, NULL, NULL, + NULL, NULL, NULL, NULL, true); + qmp_request_free(req_obj); + monitor_qmp_destroy(mon); + monitor_fdsets_cleanup(); + qmp_dispatcher_current_mon =3D NULL; + continue; + } + if (!oob_enabled) { monitor_resume(&mon->common); } =20 qmp_request_free(req_obj); + qmp_dispatcher_current_mon =3D NULL; } qatomic_set(&qmp_dispatcher_co, NULL); } @@ -499,6 +525,44 @@ void monitor_data_destroy_qmp(MonitorQMP *mon) g_queue_free(mon->qmp_requests); } =20 +static void monitor_qmp_iothread_quiesce(void *opaque) +{ + /* No-op: synchronization point only */ +} + +/* + * Destroy a single QMP monitor. + * The monitor must already have been removed from mon_list. + */ +void monitor_qmp_destroy(MonitorQMP *mon) +{ + qemu_bh_delete(mon->common.accept_input_bh); + mon->common.accept_input_bh =3D NULL; + + WITH_QEMU_LOCK_GUARD(&mon->common.mon_lock) { + /* Disable flushes before cancel -- gcontext is already wrong. */ + mon->common.skip_flush =3D true; + monitor_cancel_out_watch(&mon->common); + } + + /* Synchronize with in-flight iothread callbacks. */ + if (mon->common.use_io_thread) { + aio_wait_bh_oneshot(iothread_get_aio_context(mon_iothread), + monitor_qmp_iothread_quiesce, NULL); + } + + /* Catch requests from a racing monitor_qmp_read(). */ + monitor_qmp_drain_queue(mon); + + monitor_data_destroy(&mon->common); + g_free(mon); +} + +bool monitor_qmp_dispatcher_is_servicing(MonitorQMP *mon) +{ + return qmp_dispatcher_current_mon =3D=3D mon; +} + static void monitor_qmp_setup_handlers_bh(void *opaque) { MonitorQMP *mon =3D opaque; @@ -510,6 +574,7 @@ static void monitor_qmp_setup_handlers_bh(void *opaque) qemu_chr_fe_set_handlers(&mon->common.chr, monitor_can_read, monitor_qmp_read, monitor_qmp_event, NULL, &mon->common, context, true); + qatomic_set(&mon->setup_pending, false); } =20 void monitor_init_qmp(Chardev *chr, bool pretty, const char *id, @@ -557,6 +622,7 @@ void monitor_init_qmp(Chardev *chr, bool pretty, const = char *id, * since chardev might be running in the monitor I/O * thread. Schedule a bottom half. */ + mon->setup_pending =3D true; aio_bh_schedule_oneshot(iothread_get_aio_context(mon_iothread), monitor_qmp_setup_handlers_bh, mon); /* Synchronous insert for immediate duplicate detection. */ --=20 2.47.3 From nobody Sat Apr 11 17:09:10 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1775719204; cv=none; d=zohomail.com; s=zohoarc; b=gNcJqnDnxJ0dcG87qtgcIl8k30VMcYcW1kbAelCOfMbc/1iB1t8iO925f3grwM4plTcuhv/PknhcFY0rgzBR8s6qQyARGo1aX99sDkOQZuSz2nT24StRemNgwy3IjPi9rgqYWydtJFwXcj1ACN4Hn0VVwNuI0AdmSIWag3MUFtw= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775719204; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=H6bh0SyMhT8AZCMyAPFqsq3FOZz7s7VE6wY3oW64Hro=; b=KebPoYH70zVUyOCHDeTTTnf0WSbFq0zxIX/Hh0nbshdITFX8uE7kCsP04+26j2UpiDH7kA/E2+CGKl/93fJDJEtnupA+rj9L4J57mKD0ld53pZN4U+8CcbqlTXYMGQGvmG6gbPi6xd1KYOF1TBy7MzIBxh/jn93PgVYAMA4vPws= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775719204349718.4244469270647; Thu, 9 Apr 2026 00:20:04 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wAjfJ-0007Mq-Qf; Thu, 09 Apr 2026 03:19:01 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjfA-0007Lk-CP for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:52 -0400 Received: from tor.source.kernel.org ([172.105.4.254]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf6-0000ZG-8M for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:51 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id 79B4760125; Thu, 9 Apr 2026 07:18:40 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 05F92C4CEF7; Thu, 9 Apr 2026 07:18:37 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775719120; bh=vfa+TrNtrxBo2wYtO9pePlx356JM3/1McvzEe45+0lQ=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=R+I7mmn2eGGTUwosgGWt6r6ZOVNsu2rSXUZfjCAk3GDGsF8UIP7uAJfnJ0QD4S0KR gF3CqwpNEMP49dFiPFNpfdHN5/9Tybg64S2u9SnIYkOSQLScFSOV+MofJZQLm44Tj1 C/kTLWVTS/oSrRZysDZ88bELHl4jf3w8iucR5Xcn6OoQhSurPAtuXf9huzXx2gZjdH KZrD3M8+BoMul2zEOiFgG+Cb2tntKygu9TujWZZxzsCymYlzDONIEx7UcGUCeBlAn2 mKS070yXoUUAupKvOjojrCaJdnVhiAEwcplvZhahugt35wu+XCts1QKjC+UhAZLS68 R+U13ZDEjuFvg== From: Christian Brauner Date: Thu, 09 Apr 2026 09:18:20 +0200 Subject: [PATCH v4 3/5] qapi: add monitor-add, monitor-remove, query-monitors commands MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260409-work-qmp-monitor-hotplug-v4-3-89c4fdf69df1@kernel.org> References: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> In-Reply-To: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> To: qemu-devel@nongnu.org Cc: Markus Armbruster , Eric Blake , Fabiano Rosas , Laurent Vivier , Paolo Bonzini , Thomas Huth , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Christian Brauner X-Mailer: b4 0.16-dev X-Developer-Signature: v=1; a=openpgp-sha256; l=7958; i=brauner@kernel.org; h=from:subject:message-id; bh=vfa+TrNtrxBo2wYtO9pePlx356JM3/1McvzEe45+0lQ=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWReDzo6N1ry61WT19qz7A5qaSZy7atdpcUTcKVNak+Sx EOelb4rOkpZGMS4GGTFFFkc2k3C5ZbzVGw2ytSAmcPKBDKEgYtTACayT5aR4c1WjbpHno/lp3iz SzvPmJt3olRAu2n+jcn1kxe+UJY/WsHwi+m8RNo3/ufsO9ex/ymzLSqwc4g8Y3Oy6uqE9JyEuX7 BjAA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=172.105.4.254; envelope-from=brauner@kernel.org; helo=tor.source.kernel.org X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @kernel.org) X-ZM-MESSAGEID: 1775719207255154100 Add QMP commands for monitor lifecycle management: - monitor-add: Create a QMP monitor on an existing chardev at runtime. The chardev must exist and not already have a frontend attached (enforced by qemu_chr_fe_init()). The new monitor starts in capability negotiation mode. - monitor-remove: Remove a QMP monitor. The removal sequence is: 1. Mark dead and remove from mon_list under monitor_lock. This must happen before disconnecting chardev handlers to prevent event broadcast from calling monitor_flush_locked() after the gcontext reset, which would create an out_watch on the wrong GMainContext (see monitor_cancel_out_watch()). 2. Cancel any pending out_watch while gcontext still points to the correct context. 3. Disconnect chardev handlers. For self-removal, preserve gcontext by passing the iothread GMainContext and keep the chardev connection open so the command response can be flushed. For normal removal, pass context=3DNULL and close the connection. 4. Drain pending requests from any in-flight monitor_qmp_read(). 5. For self-removal, defer destruction to the dispatcher (the response must be flushed first). Otherwise destroy immediately via monitor_qmp_destroy(). - query-monitors: Introspect all active monitors with their id, mode, and chardev name. The motivating use case is systemd-vmspawn: when an external client requests raw QMP access, vmspawn can create an independent QMP session on demand rather than pre-allocating spare monitors at launch or building an id-rewriting proxy. Signed-off-by: Christian Brauner (Amutable) --- monitor/qmp-cmds-control.c | 105 +++++++++++++++++++++++++++++++++++++++++= ++++ qapi/control.json | 97 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/monitor/qmp-cmds-control.c b/monitor/qmp-cmds-control.c index 150ca9f5cb..33d02c8fb6 100644 --- a/monitor/qmp-cmds-control.c +++ b/monitor/qmp-cmds-control.c @@ -219,3 +219,108 @@ SchemaInfoList *qmp_query_qmp_schema(Error **errp) } return schema; } + +void qmp_monitor_add(const char *id, const char *chardev, + bool has_pretty, bool pretty, Error **errp) +{ + Chardev *chr; + + /* Reject duplicate monitor id */ + if (monitor_find_by_id(id)) { + error_setg(errp, "monitor '%s' already exists", id); + return; + } + + chr =3D qemu_chr_find(chardev); + if (!chr) { + error_setg(errp, "chardev '%s' not found", chardev); + return; + } + + monitor_init_qmp(chr, has_pretty && pretty, id, errp); +} + +void qmp_monitor_remove(const char *id, Error **errp) +{ + Monitor *mon; + MonitorQMP *qmp_mon; + bool self_remove; + + mon =3D monitor_find_by_id(id); + if (!mon) { + error_setg(errp, "monitor '%s' not found", id); + return; + } + + if (!mon->is_qmp) { + error_setg(errp, "monitor '%s' is not a QMP monitor", id); + return; + } + + qmp_mon =3D container_of(mon, MonitorQMP, common); + + if (qatomic_read(&qmp_mon->setup_pending)) { + error_setg(errp, "monitor '%s' is still initializing", id); + return; + } + + self_remove =3D monitor_qmp_dispatcher_is_servicing(qmp_mon); + + /* Remove from mon_list before chardev disconnect. */ + WITH_QEMU_LOCK_GUARD(&monitor_lock) { + mon->dead =3D true; + QTAILQ_REMOVE(&mon_list, mon, entry); + } + + /* Cancel out_watch while gcontext still points to the right ctx. */ + WITH_QEMU_LOCK_GUARD(&mon->mon_lock) { + monitor_cancel_out_watch(mon); + } + + /* + * Clear chardev handlers. Self-removal preserves gcontext so the + * response flush creates out_watch on the correct GMainContext. + */ + if (self_remove) { + GMainContext *ctx =3D mon->use_io_thread + ? iothread_get_g_main_context(mon_iothread) : NULL; + + qemu_chr_fe_set_handlers(&mon->chr, NULL, NULL, NULL, NULL, + NULL, ctx, false); + } else { + qemu_chr_fe_set_handlers(&mon->chr, NULL, NULL, NULL, NULL, + NULL, NULL, true); + } + + /* Drain requests from any in-flight monitor_qmp_read(). */ + monitor_qmp_drain_queue(qmp_mon); + + /* Self-removal: defer destruction so the response is flushed first. */ + if (self_remove) { + return; + } + + monitor_qmp_destroy(qmp_mon); + monitor_fdsets_cleanup(); +} + +MonitorInfoList *qmp_query_monitors(Error **errp) +{ + MonitorInfoList *list =3D NULL; + Monitor *mon; + + WITH_QEMU_LOCK_GUARD(&monitor_lock) { + QTAILQ_FOREACH(mon, &mon_list, entry) { + MonitorInfo *info =3D g_new0(MonitorInfo, 1); + Chardev *chr =3D qemu_chr_fe_get_driver(&mon->chr); + + info->id =3D g_strdup(mon->id); + info->mode =3D mon->is_qmp ? MONITOR_MODE_CONTROL + : MONITOR_MODE_READLINE; + info->chardev =3D g_strdup(chr ? chr->label : "unknown"); + QAPI_LIST_PREPEND(list, info); + } + } + + return list; +} diff --git a/qapi/control.json b/qapi/control.json index 9a5302193d..eb4a30b59e 100644 --- a/qapi/control.json +++ b/qapi/control.json @@ -211,3 +211,100 @@ '*pretty': 'bool', 'chardev': 'str' } } + +## +# @monitor-add: +# +# Add a QMP monitor on an existing character device backend. +# +# The chardev must already exist (created via chardev-add or CLI). +# The monitor begins in capability negotiation mode -- the first +# client to connect receives the QMP greeting. +# +# @id: Monitor identifier, must be unique among monitors +# +# @chardev: Name of the character device backend to attach to +# +# @pretty: Enable pretty-printing of QMP responses (default: false) +# +# Errors: +# - If @id is already in use +# - If @chardev does not exist +# +# Since: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "monitor-add", +# "arguments": { "id": "extra-qmp", +# "chardev": "qmp-extra" } } +# <- { "return": {} } +## +{ 'command': 'monitor-add', + 'data': { 'id': 'str', + 'chardev': 'str', + '*pretty': 'bool' } } + +## +# @monitor-remove: +# +# Remove a QMP monitor. +# +# The underlying chardev is NOT removed -- use chardev-remove +# separately if desired. +# +# If a client is currently connected, the connection is dropped. +# +# @id: Monitor identifier +# +# Errors: +# - If @id does not exist +# - If the monitor is not a QMP monitor +# +# Since: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "monitor-remove", +# "arguments": { "id": "extra-qmp" } } +# <- { "return": {} } +## +{ 'command': 'monitor-remove', + 'data': { 'id': 'str' } } + +## +# @MonitorInfo: +# +# Information about a QMP/HMP monitor. +# +# @id: Monitor identifier. Always present for QMP monitors (auto- +# generated if not explicitly set). Absent for HMP monitors. +# +# @mode: Monitor mode (readline or control) +# +# @chardev: Name of the attached character device +# +# Since: 11.1 +## +{ 'struct': 'MonitorInfo', + 'data': { '*id': 'str', + 'mode': 'MonitorMode', + 'chardev': 'str' } } + +## +# @query-monitors: +# +# Return information about all active monitors. +# +# Returns: a list of @MonitorInfo for each active monitor +# +# Since: 11.1 +# +# .. qmp-example:: +# +# -> { "execute": "query-monitors" } +# <- { "return": [ { "id": "mon0", "mode": "control", +# "chardev": "compat_monitor0" } ] } +## +{ 'command': 'query-monitors', + 'returns': ['MonitorInfo'] } --=20 2.47.3 From nobody Sat Apr 11 17:09:10 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1775719206; cv=none; d=zohomail.com; s=zohoarc; b=HDNKXqlVAQcgAEnNbpD4sGTo9ExX13bNBp3Hrs+WHINjAWVBa/q6y/HJqzy7aiV2IBnsvBMTE1QiJ3CGU+IcRmQew6oBG8SUzKO0A7WnSLNsad5VEW2ppAcOMPGA//AMfoiC1gy3kiv3XVs3Cor3HbzVarjfcpObsnbG1D4972A= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775719206; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=w9zWNF7Hn0/3LJmlhc7fwts5sPkjhyXFttjCRHk3idM=; b=WNlYqAFM32l3vf6IAaooQPIqYKS0vLAL4jgbad9Ep0eNH+jm08GQkIXYyNjIUFo9Y8m9dnkeDNjk6lRIt5sDCI4vnjLRQJWOVCJX9Q73YAXJX3g03gv/tkduOCNJecq34o0Uh2hfCSIIkgHeAx+8LPoX1dFCySkQKtacdjmnLSs= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775719206699907.2568576651619; Thu, 9 Apr 2026 00:20:06 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wAjfE-0007MA-8B; Thu, 09 Apr 2026 03:18:57 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf8-0007LE-Ab for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:50 -0400 Received: from sea.source.kernel.org ([172.234.252.31]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf2-0000ZU-1Z for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:46 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by sea.source.kernel.org (Postfix) with ESMTP id 16AB14083E; Thu, 9 Apr 2026 07:18:43 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id A46F5C116C6; Thu, 9 Apr 2026 07:18:40 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775719123; bh=eFQHDBLnQd7WByUYlLzT6pUo4auGgUaiOW6f2kX44DI=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=M11ggZ7JYY42QzswapCEZAr0qpNgLGAo1ZTgvuImxvjwPSjGpd+iA/oGFa6826gsW 6xWbp/FGTMCaOemKZdGeHFqQUF4Rdst7UP1IA9YvZ6KTlfJIa4QPApk3HZmBzCq7KX 9LCXH6EroGwqMFo6AjXkiiF/yGIrfc2u0BFCMafQVkx7zeqKGM2Ehw0zmNYLwhZF6R EYaDdNZIBkEUIhTrNBTJu6AJbnhgxY2LBecf1bShefAJBZAQfPPOvkTyBgo8ySNvOi KhsCK1IvZ95BrUyvmS5IX5/aecN9qN6a0GHidPSXOlUUyIDStn5oC8Y1nB1csqMKK9 zH1E3H2eBEjxQ== From: Christian Brauner Date: Thu, 09 Apr 2026 09:18:21 +0200 Subject: [PATCH v4 4/5] tests/qtest: add tests for dynamic monitor add/remove MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260409-work-qmp-monitor-hotplug-v4-4-89c4fdf69df1@kernel.org> References: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> In-Reply-To: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> To: qemu-devel@nongnu.org Cc: Markus Armbruster , Eric Blake , Fabiano Rosas , Laurent Vivier , Paolo Bonzini , Thomas Huth , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Christian Brauner X-Mailer: b4 0.16-dev X-Developer-Signature: v=1; a=openpgp-sha256; l=8185; i=brauner@kernel.org; h=from:subject:message-id; bh=eFQHDBLnQd7WByUYlLzT6pUo4auGgUaiOW6f2kX44DI=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWReDzrav6m1kK38pWbvnYV2sZuULWxLw6becH94ewJ3c pD4tq5fHaUsDGJcDLJiiiwO7Sbhcst5KjYbZWrAzGFlAhnCwMUpABNhrmRkmG/7fA6/ctOHkoO+ EXuPXrp5iWM9b93OwAntl9STf69iOcbIcFW8zDFt575Pgmwx4ZcMs5jqhbadORBr/ufGn1efhfT 2cgAA X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=172.234.252.31; envelope-from=brauner@kernel.org; helo=sea.source.kernel.org X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @kernel.org) X-ZM-MESSAGEID: 1775719209048158500 Test the monitor-add, monitor-remove, and query-monitors QMP commands: - Basic lifecycle: chardev-add -> monitor-add -> query-monitors -> monitor-remove -> chardev-remove - Error: duplicate monitor id - Error: monitor-remove on nonexistent id - Error: monitor-add with nonexistent chardev - Error: second monitor on same chardev (chardev already in use) - Removal of CLI-created QMP monitor succeeds - Error: monitor-remove on HMP monitor - Re-add after remove: same id and chardev reusable after removal Signed-off-by: Christian Brauner (Amutable) Reviewed-by: Fabiano Rosas --- tests/qtest/qmp-test.c | 193 +++++++++++++++++++++++++++++++++++++++++++++= ++++ 1 file changed, 193 insertions(+) diff --git a/tests/qtest/qmp-test.c b/tests/qtest/qmp-test.c index edf0886787..3ff6de3626 100644 --- a/tests/qtest/qmp-test.c +++ b/tests/qtest/qmp-test.c @@ -337,6 +337,193 @@ static void test_qmp_missing_any_arg(void) qtest_quit(qts); } =20 +static void test_qmp_monitor_add_remove(void) +{ + QTestState *qts; + QDict *resp; + + qts =3D qtest_init(common_args); + + /* Create a null chardev for the dynamic monitor */ + resp =3D qtest_qmp(qts, + "{'execute': 'chardev-add'," + " 'arguments': {'id': 'monitor-chardev'," + " 'backend': {'type': 'null'," + " 'data': {}}}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Add a dynamic monitor */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'dyn-mon'," + " 'chardev': 'monitor-chardev'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Verify it shows up in query-monitors */ + resp =3D qtest_qmp(qts, "{'execute': 'query-monitors'}"); + g_assert(!qdict_haskey(resp, "error")); + qobject_unref(resp); + + /* Error: duplicate id */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'dyn-mon'," + " 'chardev': 'monitor-chardev'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + /* Remove the dynamic monitor */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'dyn-mon'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Error: remove nonexistent */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'dyn-mon'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + /* Add again after remove -- same id and chardev should work */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'dyn-mon'," + " 'chardev': 'monitor-chardev'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Clean up */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'dyn-mon'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + resp =3D qtest_qmp(qts, + "{'execute': 'chardev-remove'," + " 'arguments': {'id': 'monitor-chardev'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + qtest_quit(qts); +} + +static void test_qmp_monitor_error_paths(void) +{ + QTestState *qts; + QDict *resp; + + qts =3D qtest_init(common_args); + + /* Error: chardev does not exist */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'bad-mon'," + " 'chardev': 'nonexistent'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + /* Error: remove nonexistent monitor */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'bogus'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + qtest_quit(qts); +} + +static void test_qmp_monitor_chardev_in_use(void) +{ + QTestState *qts; + QDict *resp; + + qts =3D qtest_init(common_args); + + /* Create a null chardev */ + resp =3D qtest_qmp(qts, + "{'execute': 'chardev-add'," + " 'arguments': {'id': 'shared-chr'," + " 'backend': {'type': 'null'," + " 'data': {}}}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Attach first monitor */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'mon-1'," + " 'chardev': 'shared-chr'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + /* Error: second monitor on the same chardev */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-add'," + " 'arguments': {'id': 'mon-2'," + " 'chardev': 'shared-chr'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + /* Clean up */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'mon-1'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + resp =3D qtest_qmp(qts, + "{'execute': 'chardev-remove'," + " 'arguments': {'id': 'shared-chr'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + qtest_quit(qts); +} + +static void test_qmp_monitor_remove_cli(void) +{ + QTestState *qts; + QDict *resp; + + /* Launch with a named CLI monitor on a null chardev */ + qts =3D qtest_initf("%s -chardev null,id=3Dcli-chr" + " -mon id=3Dcli-mon,chardev=3Dcli-chr,mode=3Dcontrol= ", + common_args); + + /* CLI-created QMP monitors can be removed */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'cli-mon'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + resp =3D qtest_qmp(qts, + "{'execute': 'chardev-remove'," + " 'arguments': {'id': 'cli-chr'}}"); + g_assert(qdict_haskey(resp, "return")); + qobject_unref(resp); + + qtest_quit(qts); +} + +static void test_qmp_monitor_remove_hmp(void) +{ + QTestState *qts; + QDict *resp; + + qts =3D qtest_initf("%s -chardev null,id=3Dhmp-chr" + " -mon id=3Dhmp-mon,chardev=3Dhmp-chr,mode=3Dreadlin= e", + common_args); + + /* Error: monitor-remove must reject HMP monitors */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'hmp-mon'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + qtest_quit(qts); +} + int main(int argc, char *argv[]) { g_test_init(&argc, &argv, NULL); @@ -348,6 +535,12 @@ int main(int argc, char *argv[]) #endif qtest_add_func("qmp/preconfig", test_qmp_preconfig); qtest_add_func("qmp/missing-any-arg", test_qmp_missing_any_arg); + qtest_add_func("qmp/monitor-add-remove", test_qmp_monitor_add_remove); + qtest_add_func("qmp/monitor-error-paths", test_qmp_monitor_error_paths= ); + qtest_add_func("qmp/monitor-chardev-in-use", + test_qmp_monitor_chardev_in_use); + qtest_add_func("qmp/monitor-remove-cli", test_qmp_monitor_remove_cli); + qtest_add_func("qmp/monitor-remove-hmp", test_qmp_monitor_remove_hmp); =20 return g_test_run(); } --=20 2.47.3 From nobody Sat Apr 11 17:09:10 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=kernel.org ARC-Seal: i=1; a=rsa-sha256; t=1775719207; cv=none; d=zohomail.com; s=zohoarc; b=RTGw64Uf5fvTQkgUTRLAkSlIVc9a0x/LE+o5oyir4uYni3bkiLE3WbTauJsM+gad7+0VJC+VnAnplxyZUY7iUyni2sZLV5E1T/47Eyaq0NvmCiZRLAxxFNoB7zb8vtIwz5qOfdmN+xQRjNdFfrG08AhWIxayu1dGC21dpQZCRu8= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775719207; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=DJSrh+0nDS60YSLJfYgg3cPoVa7txkbyAfgsvkxzjHo=; b=bIrShtdJCuJ4zznzU4D59J3Jh5bZJo82Y/4o95tS0H2QijAYay/+5vnl9aky0YDM1MsM9XuZB56gmbS0SbPAk7dd6dZl8pidTQqWW1VhRcYGJo2z8+kRB3yLn8dUJXRhkfmjiwVSupvnT2WEbEqkW6QFtJYRnRuAWtk7RRIxxLg= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775719207573594.108633932656; Thu, 9 Apr 2026 00:20:07 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wAjfM-0007NK-3E; Thu, 09 Apr 2026 03:19:04 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjfC-0007Lw-Aa for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:54 -0400 Received: from tor.source.kernel.org ([2600:3c04:e001:324:0:1991:8:25]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wAjf6-0000Zr-8h for qemu-devel@nongnu.org; Thu, 09 Apr 2026 03:18:53 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id 248EF600AD; Thu, 9 Apr 2026 07:18:46 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 6A480C4AF09; Thu, 9 Apr 2026 07:18:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775719125; bh=UlM9cP+a9nxMLP9UAsL1Afv6gPPfoTX0z4wL3Mi4BwE=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=LYVyzR2RAT/hlQ6RkAzdPir5QjnF4P8o3YQwRIJoSm9dDmhfpt2iBhjNci0XFuDoz /Id20XxVaiEXyoHtTbjOMrRvZz53Hmh/pA/EPRyblfiaOLJZz+iU2HQcYQB8TLbMfB Pqp9Zn0oMNeEZwn3eozh5ObFd1CsNVd0PelytDw/f+l3XKtpwIWUQlOdKU96uqswpM CssjxIpdU1RnFMvxYmo8c04NeotTBZd6SJl84i8pvYX3O0DS6Te+Qv2K+X8aEBCS3Q y45QCL0bm8JkLIQpt6cL+BHJIrNkuyTbsKBUSb3nC/25iVVyfLPMq6bPrBq1OAGU85 n9JCsR619XDiQ== From: Christian Brauner Date: Thu, 09 Apr 2026 09:18:22 +0200 Subject: [PATCH v4 5/5] tests/functional: add e2e test for dynamic QMP monitor hotplug MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260409-work-qmp-monitor-hotplug-v4-5-89c4fdf69df1@kernel.org> References: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> In-Reply-To: <20260409-work-qmp-monitor-hotplug-v4-0-89c4fdf69df1@kernel.org> To: qemu-devel@nongnu.org Cc: Markus Armbruster , Eric Blake , Fabiano Rosas , Laurent Vivier , Paolo Bonzini , Thomas Huth , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= , Christian Brauner X-Mailer: b4 0.16-dev X-Developer-Signature: v=1; a=openpgp-sha256; l=7400; i=brauner@kernel.org; h=from:subject:message-id; bh=UlM9cP+a9nxMLP9UAsL1Afv6gPPfoTX0z4wL3Mi4BwE=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWReDzp6oYipsvTTjF0tMrWcl2ee/fFy51qxmyfCbR+ZL CpfnzCdoaOUhUGMi0FWTJHFod0kXG45T8Vmo0wNmDmsTCBDGLg4BWAisvMYGSa1cwTppkX3P+C5 Enxh/YoLHjPK5Dli95ozfUk07vdxtWZk+CX+8WTIemdHXZaqiZcZhKLtd8W87BfeI7VPWaTHe7s dKwA= X-Developer-Key: i=brauner@kernel.org; a=openpgp; fpr=4880B8C9BD0E5106FC070F4F7B3C391EFEA93624 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=2600:3c04:e001:324:0:1991:8:25; envelope-from=brauner@kernel.org; helo=tor.source.kernel.org X-Spam_score_int: -25 X-Spam_score: -2.6 X-Spam_bar: -- X-Spam_report: (-2.6 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @kernel.org) X-ZM-MESSAGEID: 1775719209177158500 Add functional tests that exercise dynamic monitor hotplug with real socket connections: - Hotplug cycle: chardev-add a unix socket, monitor-add, connect to the socket, receive the QMP greeting, negotiate capabilities, send query-version, disconnect, remove the monitor and chardev, then repeat the entire cycle a second time to verify cleanup and reuse. - Self-removal: a dynamically-added monitor sends monitor-remove targeting itself, verifying that the response is delivered before the connection drops and that the monitor is gone afterwards. - Large response: send query-qmp-schema on a dynamic monitor to exercise the output buffer flush path with a large response payload. - Events after negotiation: trigger STOP/RESUME events via the main monitor and verify they are delivered on the dynamic monitor. This complements the qtest unit tests by verifying that a real QMP client can connect to a dynamically-added monitor and exchange messages. Signed-off-by: Christian Brauner (Amutable) --- tests/functional/generic/meson.build | 1 + tests/functional/generic/test_monitor_hotplug.py | 170 +++++++++++++++++++= ++++ 2 files changed, 171 insertions(+) diff --git a/tests/functional/generic/meson.build b/tests/functional/generi= c/meson.build index 09763c5d22..c94105c62e 100644 --- a/tests/functional/generic/meson.build +++ b/tests/functional/generic/meson.build @@ -4,6 +4,7 @@ tests_generic_system =3D [ 'empty_cpu_model', 'info_usernet', 'linters', + 'monitor_hotplug', 'version', 'vnc', ] diff --git a/tests/functional/generic/test_monitor_hotplug.py b/tests/funct= ional/generic/test_monitor_hotplug.py new file mode 100644 index 0000000000..f81236e7b4 --- /dev/null +++ b/tests/functional/generic/test_monitor_hotplug.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Functional test for dynamic QMP monitor hotplug +# +# Copyright (c) 2026 Christian Brauner + +import os + +from qemu_test import QemuSystemTest + +from qemu.qmp.legacy import QEMUMonitorProtocol + + +class MonitorHotplug(QemuSystemTest): + + def setUp(self): + super().setUp() + sock_dir =3D self.socket_dir() + self._sock_path =3D os.path.join(sock_dir.name, 'hotplug.sock') + + def _add_monitor(self): + """Create a chardev + monitor and return the socket path.""" + sock =3D self._sock_path + self.vm.cmd('chardev-add', id=3D'hotplug-chr', backend=3D{ + 'type': 'socket', + 'data': { + 'addr': { + 'type': 'unix', + 'data': {'path': sock} + }, + 'server': True, + 'wait': False, + } + }) + self.vm.cmd('monitor-add', id=3D'hotplug-mon', + chardev=3D'hotplug-chr') + return sock + + def _remove_monitor(self): + """Remove the monitor + chardev.""" + self.vm.cmd('monitor-remove', id=3D'hotplug-mon') + self.vm.cmd('chardev-remove', id=3D'hotplug-chr') + + def _connect_and_handshake(self, sock_path): + """ + Connect to the dynamic monitor socket, perform the QMP + greeting and capability negotiation, send a command, then + disconnect. + """ + qmp =3D QEMUMonitorProtocol(sock_path) + + # connect(negotiate=3DTrue) receives the greeting, validates it, + # and sends qmp_capabilities automatically. + greeting =3D qmp.connect(negotiate=3DTrue) + self.assertIn('QMP', greeting) + self.assertIn('version', greeting['QMP']) + self.assertIn('capabilities', greeting['QMP']) + + # Send a real command to prove the session is fully functional + resp =3D qmp.cmd_obj({'execute': 'query-version'}) + self.assertIn('return', resp) + self.assertIn('qemu', resp['return']) + + qmp.close() + + def test_hotplug_cycle(self): + """ + Hotplug a monitor, do the full QMP handshake, unplug it, + then repeat the whole cycle a second time. + """ + self.set_machine('none') + self.vm.add_args('-nodefaults') + self.vm.launch() + + # First cycle + sock =3D self._add_monitor() + self._connect_and_handshake(sock) + self._remove_monitor() + + # Second cycle -- same ids, same path, must work + sock =3D self._add_monitor() + self._connect_and_handshake(sock) + self._remove_monitor() + + def test_self_removal(self): + """ + A dynamically-added monitor sends monitor-remove targeting + itself. Verify the response is delivered before the + connection drops, and that the monitor is gone afterwards. + """ + self.set_machine('none') + self.vm.add_args('-nodefaults') + self.vm.launch() + + sock =3D self._add_monitor() + + qmp =3D QEMUMonitorProtocol(sock) + greeting =3D qmp.connect(negotiate=3DTrue) + self.assertIn('QMP', greeting) + + # Self-removal: the dynamic monitor removes itself + resp =3D qmp.cmd_obj({'execute': 'monitor-remove', + 'arguments': {'id': 'hotplug-mon'}}) + self.assertIn('return', resp) + + qmp.close() + + # The main monitor should no longer list the removed monitor + monitors =3D self.vm.cmd('query-monitors') + for m in monitors: + self.assertNotEqual(m.get('id'), 'hotplug-mon') + + # Clean up the chardev + self.vm.cmd('chardev-remove', id=3D'hotplug-chr') + + def test_large_response(self): + """ + Send a command with a large response (query-qmp-schema) on a + dynamically-added monitor to exercise the output buffer flush + path. + """ + self.set_machine('none') + self.vm.add_args('-nodefaults') + self.vm.launch() + + sock =3D self._add_monitor() + + qmp =3D QEMUMonitorProtocol(sock) + qmp.connect(negotiate=3DTrue) + + resp =3D qmp.cmd_obj({'execute': 'query-qmp-schema'}) + self.assertIn('return', resp) + self.assertIsInstance(resp['return'], list) + self.assertGreater(len(resp['return']), 0) + + qmp.close() + self._remove_monitor() + + def test_events_after_negotiation(self): + """ + Verify that QMP events are delivered on a dynamically-added + monitor after capability negotiation completes. + """ + self.set_machine('none') + self.vm.add_args('-nodefaults') + self.vm.launch() + + sock =3D self._add_monitor() + + qmp =3D QEMUMonitorProtocol(sock) + qmp.connect(negotiate=3DTrue) + + # Trigger a STOP event via the main monitor, then read it + # from the dynamic monitor. + self.vm.cmd('stop') + resp =3D qmp.pull_event(wait=3DTrue) + self.assertEqual(resp['event'], 'STOP') + + self.vm.cmd('cont') + resp =3D qmp.pull_event(wait=3DTrue) + self.assertEqual(resp['event'], 'RESUME') + + qmp.close() + self._remove_monitor() + + +if __name__ =3D=3D '__main__': + QemuSystemTest.main() --=20 2.47.3