From nobody Sat Apr 11 18:36:44 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=1775590560; cv=none; d=zohomail.com; s=zohoarc; b=A8XGKqUSLptQRdw2OYPYhTk9KKl96FEbyadgCZM7XdaQ+s3oQBuHWUB+k3JVAFq4z/Q+Lwn+HQCb4sYusqOraisii+9rSsMLgrz4uCLFM5CNMvt6mfhQqZbMKH8X2vBptntuealMr89Ofw4lsvkiDY6hn2skDL5cr8sIKObQLZM= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775590560; 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=cATgIF93TKG8VSpfCF1bG74fd65o8z3NdGd2aTGAe1E=; b=JSRlxuNzs9KGZUeD19eyqw+FAqeLK20VcpziOrJymAqjj9hhgYn5GW5n1Egl7eHILqSWAkrSAiuXRObw1rFIaU3k09bQKOhcOyq/1SMiJXYpvKsqxRlIhmUxik/zkR/Zl0SWjOghW+J3SV/nybQ62BhqyqR//vLm8SFDVSWD/Ss= 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 (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775590560705644.8492168145681; Tue, 7 Apr 2026 12:36:00 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wABXH-00008a-M7; Tue, 07 Apr 2026 14:52:27 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wABTx-0002iK-6r for qemu-devel@nongnu.org; Tue, 07 Apr 2026 14:49:01 -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 1wA0vo-0007bc-Tw for qemu-devel@nongnu.org; Tue, 07 Apr 2026 03:33:06 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by sea.source.kernel.org (Postfix) with ESMTP id 92EFE434F8; Tue, 7 Apr 2026 07:33:03 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id 27962C2BC9E; Tue, 7 Apr 2026 07:33:00 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775547183; bh=G81uyv8r5lRSGGkc7QTHuaYAIIqRbuYMdg2xyWaslgI=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=abaXlLO2QBi3KtcwXrha7OiatW8C8D3wyeoVOtM+kbqHcHk7PSXU45fyY4LN/kfbc KHzu7+Y+n6Edp4w7Fttl3pWC5AsCABeckZc5pf7IjuAdwFeEEqBa95JmN41k4L+aQq 5C1islo66fOrVDzh/ALUM0qOmFLlK87s2ZYlFcmlT8mz03CcDhD3WHNmrS/7JjgfWA 6xkKkYY2G6VBGMz1qE6yBHA+qpOrzmeeDe6DlB2NqwXVG0M9qVrTo8ZIG82r+T1xxk 60d2L6RtCkRrwNv6gfAprGIULpxiTAD1xp3SBJIv7sf/Pf+IBYFn/3RRsuFVO7lQz+ u47wLOgNOysWQ== From: Christian Brauner Date: Tue, 07 Apr 2026 09:32:45 +0200 Subject: [PATCH v3 1/5] monitor: store monitor id and dynamic flag in Monitor struct MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20260407-work-qmp-monitor-hotplug-v3-1-cb259800fffb@kernel.org> References: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@kernel.org> In-Reply-To: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@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=7226; i=brauner@kernel.org; h=from:subject:message-id; bh=G81uyv8r5lRSGGkc7QTHuaYAIIqRbuYMdg2xyWaslgI=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRe2azJedBxWVpSEE9T55pXNW3eMqk7WY7NFhDOD3D6d URG9jtvRykLgxgXg6yYIotDu0m43HKeis1GmRowc1iZQIYwcHEKwERufWFkeNWwV2N3gWnpw8v3 Eh/4M1yY7T4jrmav8VS280mX07tvxDAy3JVe8nePvNA3iRyPc5xpy+femirsPfPv07M/ryyLz17 kywsA 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: 1775590561822154100 Add 'id', 'dynamic', and 'dead' fields to struct Monitor. The id field stores the monitor identifier from MonitorOptions which was previously parsed but discarded. The dynamic flag marks monitors created at runtime via the upcoming monitor-add command, and the dead flag will be used for deferred destruction during monitor-remove. Extend monitor_init_qmp() to accept id and dynamic parameters so these are 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 | 5 +++++ monitor/monitor.c | 21 ++++++++++++++++++++- monitor/qmp.c | 11 ++++++++--- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/include/monitor/monitor.h b/include/monitor/monitor.h index 296690e1f1..7a2bb603e4 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, + bool dynamic, 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..4896812d4e 100644 --- a/monitor/monitor-internal.h +++ b/monitor/monitor-internal.h @@ -98,7 +98,10 @@ struct Monitor { bool is_qmp; bool skip_flush; bool use_io_thread; + bool dynamic; /* true if created via monitor-add */ + bool dead; /* awaiting drain after monitor-remove */ =20 + char *id; /* NULL for unnamed CLI monitors */ char *mon_cpu_path; QTAILQ_ENTRY(Monitor) entry; =20 @@ -181,6 +184,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..7144255e12 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, false, errp); break; case MONITOR_MODE_READLINE: if (!allow_hmp) { diff --git a/monitor/qmp.c b/monitor/qmp.c index 687019811f..afbe2283d6 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, + bool dynamic, Error **errp) { MonitorQMP *mon =3D g_new0(MonitorQMP, 1); =20 @@ -527,12 +527,16 @@ 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 + mon->common.id =3D g_strdup(id); + mon->common.dynamic =3D dynamic; 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 +555,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 18:36:44 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=1775590716; cv=none; d=zohomail.com; s=zohoarc; b=NrkzzpnJGJb4TsaHLqgsJjgYtE02HAXjgBKqmgrdR/awkLi6VkS7QCmfnX9a451o6onS+dcNsmKeGd/1l8DMb3jCwD9Sx43Yo4IK6rXxKL8ziTels9afYBpgBH9WFGe2AKXp13yXA9B4gtozBWi1ndQN9Iq0shlXZuTwdwnaTc0= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775590716; 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=74rQ+ZzgmNrQ+LyApWnKumwmC3pOBOpG7v6gpn9eWLI=; b=i8a0bT+Mr+E0r1veouSessiuxkG+k5q/ZuQx6IEuKz2BhMmpeotQrV2Ehcz5zXUqfNL4+tKbkneHnHXwzK7KJ0L0qILviC3bbfc6RuDSlpX68mJDW4KsvqmWIAqJ981wkDJtvF1Ic4E1HGqOT/aROyGDkAS1M3EnvLONe9XaSmY= 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 (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775590716782206.6454144619072; Tue, 7 Apr 2026 12:38:36 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wABXH-000089-6I; Tue, 07 Apr 2026 14:52:27 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wABTx-0002LR-4g for qemu-devel@nongnu.org; Tue, 07 Apr 2026 14:49:01 -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 1wA0vr-0007bz-Ov for qemu-devel@nongnu.org; Tue, 07 Apr 2026 03:33:09 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id C4AB8600AC; Tue, 7 Apr 2026 07:33:06 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id E5D3FC116C6; Tue, 7 Apr 2026 07:33:03 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775547186; bh=kftrZlQfiwL6iRZ5RpAmm8uIKXWZcXoZKdMQ5NHiX0s=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=BYPd4BtIUqFEzFLP4WYx3DTTApVmT4HTrzd5Fp7uzB0AYBXj/SiAh1nx5fBXBCV1R hzOElCE0ZxPiM2yDq/Tapn8TFNvrhjpzCogqMqQsxTOMVAfUlixKjHj2E+xQWXShLG JsNlTiE2nalmNx3k5s8TZnu9GKVPlph/BlXlWr80NQVbDr4p+ntbl5PuJu6mW6taDX 3PVVMXEm5mnAm3Mhsav1MkcQjGl9pMEfiiFevYVErcy0KhIG27l47wXzue+hQBmzo4 vmI9d3+9hok4vAVX+IkLQweaP2Jp0kflq4quEVGye3hjDI1KnLJij/m02Wg6MrNxX3 yrsOoU6b5Fe5Q== From: Christian Brauner Date: Tue, 07 Apr 2026 09:32:46 +0200 Subject: [PATCH v3 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: <20260407-work-qmp-monitor-hotplug-v3-2-cb259800fffb@kernel.org> References: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@kernel.org> In-Reply-To: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@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=13101; i=brauner@kernel.org; h=from:subject:message-id; bh=kftrZlQfiwL6iRZ5RpAmm8uIKXWZcXoZKdMQ5NHiX0s=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRe2aypuvX+X7M/ibO/nyjY5L3rb+6yBcktXUycwr/m9 54xmVaQ1FHKwiDGxSArpsji0G4SLrecp2KzUaYGzBxWJpAhDFycAjART0dGhkdzthvNkBDYsuR1 wIJEsYO5fVXHlVVnHT65kjGD4YH1xnZGhq7cycqBB57sW+WSyTBRdsZmTyuuH2H5bbEzc09NFlo Vxw4A 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: 1775590718387154100 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 | 6 +++++ monitor/monitor.c | 49 ++++++++++++++++++++++++++-------- monitor/qmp.c | 66 ++++++++++++++++++++++++++++++++++++++++++= ++++ 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/monitor/monitor-internal.h b/monitor/monitor-internal.h index 4896812d4e..370bd4ad53 100644 --- a/monitor/monitor-internal.h +++ b/monitor/monitor-internal.h @@ -100,6 +100,7 @@ struct Monitor { bool use_io_thread; bool dynamic; /* true if created via monitor-add */ bool dead; /* awaiting drain after monitor-remove */ + QEMUBH *accept_input_bh; /* persistent BH for monitor_accept_input */ =20 char *id; /* NULL for unnamed CLI monitors */ char *mon_cpu_path; @@ -138,6 +139,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 @@ -176,15 +178,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 7144255e12..0080e8ca0e 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 afbe2283d6..6645b82d48 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 dynamically-added 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 =E2=80=94 gcontext is already wro= ng. */ + 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, @@ -553,6 +618,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 18:36:44 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=1775590486; cv=none; d=zohomail.com; s=zohoarc; b=TxJsMBc0/OC6Qo85223trCwfszDT0jKu4COdgAu+421Qw0D0oXlgWsFQUOSUhIsTK5oEKBXHOg2MMVgclfUHX8utouFlqTe8gTbhD+g43JnQzwBPEbcgZkjUUFLnknrI62GhC1P34Lo0fVge1sAX/e8/eu0Aq0e/yphRGvm7L2I= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775590486; 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=3Cj1Ym8AXXLMDj+KyylcxhRRr6iuJojKwzTT1yTsKEI=; b=FMkThOnok0kwM+rfnJ9IkP6d0zyr6VzYis3kvTNGIGWd9IENqHbSEx9GOTlzugGzac6d56p4hS1/uos9zvXgxHgPWwok/HIQ/gV47a2rxg0804gtQtKLsPMfmNE9839qwoOE7Wwrz/ZH1VCFwMR8uAAelOl3rMnAebbbzXhYDCE= 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 (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775590486030134.99983598806693; Tue, 7 Apr 2026 12:34:46 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wABXK-00009Z-S5; Tue, 07 Apr 2026 14:52:30 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wABTx-0002o3-33 for qemu-devel@nongnu.org; Tue, 07 Apr 2026 14:49:01 -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 1wA0vu-0007cH-LC for qemu-devel@nongnu.org; Tue, 07 Apr 2026 03:33:12 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id 831D160126; Tue, 7 Apr 2026 07:33:09 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id EF5E4C2BC9E; Tue, 7 Apr 2026 07:33:06 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775547189; bh=vUOsnjMPnEut5csZPoZP/V+/ffYrQlfAd/dULVEgPcU=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=Pi3DmgXx0xx31Ui53oezrPbjTPYc13zCMdpPbqXfRxXityA1+kKsusEaZG13oTL/D NrVaA1kzEQMHm7jkFK8cjBDl5sMkGb87B9sqKEK7kCi2HH5lyikEBB9GCUgQYA+shk bc0XHD3N/GsoE6oPmB0PPUGKd6rBPYZz9LQh+3hNtXCobiUhWeiipB0KfYll2ZJeMv e+RXAmX6tun+nA22t2Z7bFGDT4BtAxSAcUcefHjbKHvRkJno+7Gzwmwr4XuJyC/Oy+ Ipye7UA03elVxt8HYXGpyH3AOcev3Jjmo+2atstpvSZaB2cRSfBk/Zzk4Y4zxsFGiA 6FgmoF/f2y5mg== From: Christian Brauner Date: Tue, 07 Apr 2026 09:32:47 +0200 Subject: [PATCH v3 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: <20260407-work-qmp-monitor-hotplug-v3-3-cb259800fffb@kernel.org> References: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@kernel.org> In-Reply-To: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@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=8601; i=brauner@kernel.org; h=from:subject:message-id; bh=vUOsnjMPnEut5csZPoZP/V+/ffYrQlfAd/dULVEgPcU=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRe2azZ896gqph/tci+wM7tTltkG5SeMEtq5YfH3g1vX djNfGFORykLgxgXg6yYIotDu0m43HKeis1GmRowc1iZQIYwcHEKwERe32f4X+G21Xyfi+D8k4G3 HFczTfOafHDWdecEHQcHd8317oWuEgz/i7XVI3ez/9t7/cnviPzny7f9Lr35tOFINuv3Cbs3C+W v5gEA 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: 1775590488653154100 Add QMP commands for dynamic 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 dynamically-added monitor. CLI-created monitors cannot be removed. 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, chardev name, and whether they were dynamically added. 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 | 111 +++++++++++++++++++++++++++++++++++++++++= ++++ qapi/control.json | 104 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) diff --git a/monitor/qmp-cmds-control.c b/monitor/qmp-cmds-control.c index 150ca9f5cb..c0a3e03aa5 100644 --- a/monitor/qmp-cmds-control.c +++ b/monitor/qmp-cmds-control.c @@ -219,3 +219,114 @@ 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, true, 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; + } + + if (!mon->dynamic) { + error_setg(errp, "monitor '%s' was not dynamically added", 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"); + info->dynamic =3D mon->dynamic; + QAPI_LIST_PREPEND(list, info); + } + } + + return list; +} diff --git a/qapi/control.json b/qapi/control.json index 9a5302193d..fd58b57c31 100644 --- a/qapi/control.json +++ b/qapi/control.json @@ -211,3 +211,107 @@ '*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: +# - GenericError if @id is already in use +# - GenericError if @chardev does not exist +# +# Since: 11.0 +# +# .. 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 dynamically added QMP monitor. +# +# The monitor must have been created via monitor-add. Monitors +# created via CLI options (-mon, -qmp) cannot be removed. 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 as passed to monitor-add +# +# Errors: +# - GenericError if @id does not exist +# - GenericError if the monitor was not dynamically added +# +# Since: 11.0 +# +# .. 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 (absent for CLI-created monitors without +# an explicit id) +# +# @mode: Monitor mode (readline or control) +# +# @chardev: Name of the attached character device +# +# @dynamic: true if created via monitor-add (removable), false if +# created via CLI +# +# Since: 11.0 +## +{ 'struct': 'MonitorInfo', + 'data': { '*id': 'str', + 'mode': 'MonitorMode', + 'chardev': 'str', + 'dynamic': 'bool' } } + +## +# @query-monitors: +# +# Return information about all active monitors. +# +# Returns: a list of @MonitorInfo for each active monitor +# +# Since: 11.0 +# +# .. qmp-example:: +# +# -> { "execute": "query-monitors" } +# <- { "return": [ { "id": "mon0", "mode": "control", +# "chardev": "compat_monitor0", +# "dynamic": false } ] } +## +{ 'command': 'query-monitors', + 'returns': ['MonitorInfo'] } --=20 2.47.3 From nobody Sat Apr 11 18:36:44 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=1775590075; cv=none; d=zohomail.com; s=zohoarc; b=drF8ygfvlFucJ/DPKaIhVWWvNT8rgpB2amgeWpn01gAr6R3fUMD3cidN3+Nevi3cgvTLDjNSom+0GyU9V6CX8sRLKqZ2c8RxRIQc3m3qmI0ZQppkDfIiem9IPZA6S9cKgOBeGPKtMz/yjFad7dyZXbMAFzH2mv8KOD6XHF54vpk= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775590075; 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=WE17PH+khmUVUY0Mt7Vn5GbwFqL2LHzMZ0gGxURceOI=; b=MzHb15SvpQnJBUaEgvKSkxx+LzptnOku5wZxU9TNqkCz5sqC+o9nj7dTXnuvRXgAmfbwlbStSxGqZ22nF1MxOmIM5yQkguPVniS5qGE8qnejXHhaNfkmyzgOvhJTfvAx2Tek94LOwAsPsyR5mVCMIobeM05CH05ImPdY6Ljo5nc= 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 (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775590075685539.8262620568147; Tue, 7 Apr 2026 12:27:55 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wABXJ-00009D-F0; Tue, 07 Apr 2026 14:52:29 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wABTw-0003wR-Ro for qemu-devel@nongnu.org; Tue, 07 Apr 2026 14:49:00 -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 1wA0vx-0007ch-4S for qemu-devel@nongnu.org; Tue, 07 Apr 2026 03:33:14 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by tor.source.kernel.org (Postfix) with ESMTP id 7B8A5600AC; Tue, 7 Apr 2026 07:33:12 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id AE10EC2BC9E; Tue, 7 Apr 2026 07:33:09 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775547192; bh=UnUNpTfNJFNxER59mlO1GiJ99z8RBPCUc14j+NR6xGw=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=qaTG2D6WVzwt/mWIWVd0DrI+iPIfd+qtXYQgyicK5ohJLuaHDO/Zt6a3Eu+kiOnbB g6aTPMFJjZyvVgDMWlycfN2/taHyRFas+EDjQHt2g3TqByenuxWD2Gdt4pU6v+c6zt sGIZEay3OjtAHf6v7MSgn/CYqcI1+5ITXxOgXBlMTvnakCr0/IxEzKs7pVTLS+9saV FRQnr3JAXe8mtf1vgSbafVjgnkI0KxsC5IUognZc51SC1Zw4+dkb3ZFaSffTLjcQf/ hs57Hq/OKCSIUVxi/L7QtaKUkswoR+9E9tJ65TbgbwRqOO7cYEqUYgybtP+RSJl2mE LjIaRjRbhGh6A== From: Christian Brauner Date: Tue, 07 Apr 2026 09:32:48 +0200 Subject: [PATCH v3 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: <20260407-work-qmp-monitor-hotplug-v3-4-cb259800fffb@kernel.org> References: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@kernel.org> In-Reply-To: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@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=7965; i=brauner@kernel.org; h=from:subject:message-id; bh=UnUNpTfNJFNxER59mlO1GiJ99z8RBPCUc14j+NR6xGw=; b=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRe2axZxphm5HPxweojPxOarmmlaEzMq70bOyNke+CBG ecXOZb6dJSyMIhxMciKKbI4tJuEyy3nqdhslKkBM4eVCWQIAxenAEzk1jpGhin/5x2bm1R1+MP0 DNntzJHv9u5VVj8xq9TqgcV7+blzvkUxMuzKnJE4cV+Km9kvl84a6b3np/7rL15oxdwcMv1w1E+ +bkYA 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: 1775590076147158500 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) - Error: monitor-remove on CLI-created monitor - Error: monitor-remove on HMP monitor - Re-add after remove: same id and chardev reusable after removal Signed-off-by: Christian Brauner (Amutable) --- tests/qtest/qmp-test.c | 186 +++++++++++++++++++++++++++++++++++++++++++++= ++++ 1 file changed, 186 insertions(+) diff --git a/tests/qtest/qmp-test.c b/tests/qtest/qmp-test.c index edf0886787..5bd57c37cb 100644 --- a/tests/qtest/qmp-test.c +++ b/tests/qtest/qmp-test.c @@ -337,6 +337,186 @@ 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); + + /* Error: monitor-remove must reject CLI-created monitors */ + resp =3D qtest_qmp(qts, + "{'execute': 'monitor-remove'," + " 'arguments': {'id': 'cli-mon'}}"); + qmp_expect_error_and_unref(resp, "GenericError"); + + 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 +528,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 18:36:44 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=1775590561; cv=none; d=zohomail.com; s=zohoarc; b=VDv0QXVLnU+keUogIOnvgldam1kUa7ONJ4Gh9mLU3b09k4iOQYjZGQ6yYYjTYwxOWcTfMeo7FVSRMezuNJvxa9fAyQ1cvEdKU+ZlZGA/tTWyVqdmiPvpJ5n3bY0Cd37+PBlcVfLZQ5WOdRNPspG3Ik0Q9EOxs/4JzFmQRi//o5Y= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1775590561; 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=hftZRwdvy9PX1vb/5V0QQUD6rEfi8GxQoBemOp2ol2ZAl6ufeA6v4Fj20RU/W/WDq9r6DV14zPzRyGNuhA4rPcOYnDb4Af8xnokTfPcaiYdyFw1zBK8C/eGRg8IvzTAJ0JSO4PHk7SvbitrjmgSpMi/pYi5pqezk/Dug7EhsLkE= 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 (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1775590561733638.171950645223; Tue, 7 Apr 2026 12:36:01 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wABXK-00009T-Ds; Tue, 07 Apr 2026 14:52:30 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wABTw-0002eY-TV for qemu-devel@nongnu.org; Tue, 07 Apr 2026 14:49:01 -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 1wA0w0-0007cv-3l for qemu-devel@nongnu.org; Tue, 07 Apr 2026 03:33:17 -0400 Received: from smtp.kernel.org (transwarp.subspace.kernel.org [100.75.92.58]) by sea.source.kernel.org (Postfix) with ESMTP id DC95E4428D; Tue, 7 Apr 2026 07:33:14 +0000 (UTC) Received: by smtp.kernel.org (Postfix) with ESMTPSA id A6710C2BC9E; Tue, 7 Apr 2026 07:33:12 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1775547194; bh=UlM9cP+a9nxMLP9UAsL1Afv6gPPfoTX0z4wL3Mi4BwE=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=T2ot9YPux08OYQ7MdJBGjToXsPOvxU9twImPmldshjzexcq0CRyT0w96hTZ5V4dtg iFGN57Ff+2aVMIGagrRI/NgC+swTXQEG7rcw7JBGEYKDJuou+ZErX6SxJdIKaHexbT 8EljSvIJJ0mJyrbwXyzt68stg+JOTGNliLymd/gk+vQCB6Kgk/ZfdAoDRh67+I3bYh 7cpZvAqWoCZSZx/pRIE9BI/kXgx0CagKwl/WbDUEuopsK0oHDVUOEXgJQ/UhuqV9PB cLSdiS274181kc11im/MV5VYvpF8baqyniNLqBrjRibqGV5MuBK01FKtEXbV69+dUK ZBD8og6/tAjJg== From: Christian Brauner Date: Tue, 07 Apr 2026 09:32:49 +0200 Subject: [PATCH v3 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: <20260407-work-qmp-monitor-hotplug-v3-5-cb259800fffb@kernel.org> References: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@kernel.org> In-Reply-To: <20260407-work-qmp-monitor-hotplug-v3-0-cb259800fffb@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=owGbwMvMwCU28Zj0gdSKO4sYT6slMWRe2azl/V1rw5HfZYXJXIdjeQ8vvSEm5ubmITnjR0zMq o6V1owHOkpZGMS4GGTFFFkc2k3C5ZbzVGw2ytSAmcPKBDKEgYtTACYyyY2RYaLU8oiaY8ouPH5p L9b4lDDqOM2z0X7rct1z66KnFVrbghn+ysW/DJxbGpArXhd9dv2mv7xPs6sWVyRpfObXtK879Mm QCwA= 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: 1775590563010158500 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