@samitouri / QOSamiQemu / commits / 6c479e70e2

monitor: add support for auto-deleting monitors upon close

The default monitor is usually a long lived object that will exist for the entire lifetime of the VM. A monitor can only service a single client at a time though, and so it might be desirable to hotplug additional monitors at runtime for specific tasks. If doing that, however, there is a need to remove the monitor when it is no longer needed. A use case for hotplugging a monitor can involve a user wishing to spawn an ad hoc script that uses a temporary monitor. The script can ask the management application to hotplug a monitor and pass back a pre-opened FD using SCM_RIGHTS. In this case the lifetime of the script is not tied to the management application and thus it is desirable to have automatic cleanup when the script exits. Allowing a client to run "object-del" against its own monitor adds complex edge cases, as it would be desirable to send the QMP response despite the monitor sending it being deleted. Doing "object-del" alone will also result in orphaning a character device backend instance, as there is no opportunity to run the companion "chardev-del" command. A simpler way to ensure cleanup is to add the concept of auto-deleting monitor objects. Specifically when the "CHR_EVENT_CLOSED" event is emitted, the equivalent of "object-del" + "chardev-del" can be run internally. Since the transient client has already droppped its monitor connection, there is no synchronization to be concerned about with sending QMP replies. There is still some internal synchronization needed, however, between the character device event callback and the bottom-half that runs the delete. There is a chance that an incoming client connection may arise before the bottom-half runs, which has to be checked. Once the monitor object is deleted, the event callback is unregistered from the character device, eliminating any further races before the character device is fully deleted. This is implemented via a new "close-action=none|delete" property on the 'monitor-qmp' object. This concept could be extended with further actions in future, for example: * close-action=shutdown - graceful guest shutdown * close-action=terminate - immediate guest poweroff * close-action=stop - pause guest CPUs while the monitor is not connected to any client This is left as an exercise for future interested contributors. Tested-by: Peter Krempa <pkrempa@redhat.com> Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com> Acked-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> Message-ID: <20260706135824.2623960-33-berrange@redhat.com> [Commit message typos fixed] Signed-off-by: Markus Armbruster <armbru@redhat.com>

Daniel P. Berrangé committed Jul 6, 2026 at 14:58 UTC 6c479e70e29f80bfd2b72c1ed760a1ddd9600281
4 files changed +183 -5
monitor/monitor-internal.h
+3
@@ -28,6 +28,7 @@
28 #include "chardev/char-fe.h"
29 #include "monitor/monitor.h"
30 #include "qapi/qapi-types-control.h"
31 +#include "qapi/qapi-types-qom.h"
32 #include "qapi/qmp-registry.h"
33 #include "qobject/json-parser.h"
34 #include "qemu/readline.h"
@@ -178,7 +179,9 @@ struct MonitorQMP {
179 Monitor parent_obj;
180 JSONMessageParser parser;
181 bool pretty;
182 + MonitorQMPCloseAction close_action;
183 bool setup_pending; /* iothread BH has not yet set up chardev handlers */
184 + bool delete_pending; /* close_action has started 'delete' process */
185 /*
186 * When a client connects, we're in capabilities negotiation mode.
187 * @commands is &qmp_cap_negotiation_commands then. When command
monitor/qmp.c
+80
@@ -28,6 +28,7 @@
28 #include "monitor-internal.h"
29 #include "qapi/error.h"
30 #include "qapi/qapi-commands-control.h"
31 +#include "qapi/qapi-commands-char.h"
32 #include "qobject/qdict.h"
33 #include "qobject/qjson.h"
34 #include "qobject/qlist.h"
@@ -103,6 +104,20 @@ static void monitor_qmp_set_pretty(Object *obj, bool val, Error **errp)
104 mon->pretty = val;
105 }
106
107 +static int monitor_qmp_get_close_action(Object *obj, Error **errp)
108 +{
109 + MonitorQMP *mon = MONITOR_QMP(obj);
110 +
111 + return mon->close_action;
112 +}
113 +
114 +static void monitor_qmp_set_close_action(Object *obj, int val, Error **errp)
115 +{
116 + MonitorQMP *mon = MONITOR_QMP(obj);
117 +
118 + mon->close_action = val;
119 +}
120 +
121 static void monitor_qmp_emit_event(Monitor *mon, QAPIEvent event, QDict *qdict);
122 static bool monitor_qmp_requires_iothread(const Monitor *mon);
123 static void monitor_qmp_complete(UserCreatable *uc, Error **errp);
@@ -117,6 +132,11 @@ static void monitor_qmp_class_init(ObjectClass *cls, const void *data)
132 object_class_property_add_bool(cls, "pretty",
133 monitor_qmp_get_pretty,
134 monitor_qmp_set_pretty);
135 + object_class_property_add_enum(cls, "close-action",
136 + "MonitorQMPCloseAction",
137 + &MonitorQMPCloseAction_lookup,
138 + monitor_qmp_get_close_action,
139 + monitor_qmp_set_close_action);
140
141 moncls->emit_event = monitor_qmp_emit_event;
142 moncls->requires_iothread = monitor_qmp_requires_iothread;
@@ -550,11 +570,49 @@ static QDict *qmp_greeting(MonitorQMP *mon)
570 ver, cap_list);
571 }
572
573 +static void monitor_qmp_self_delete_bh(void *opaque)
574 +{
575 + MonitorQMP *mon = opaque;
576 + const char *mon_id = object_get_canonical_path_component(
577 + OBJECT(mon));
578 + g_autofree char *chardev_id = g_strdup(mon->parent_obj.chardev_id);
579 + Error *local_error = NULL;
580 +
581 + if (!mon_id) {
582 + /*
583 + * Another monitor raced & ran 'object-del' on 'mon'
584 + * before this BH got scheduled, so we have a ref on
585 + * mon from monitor_qmp_event but it is already
586 + * unparented.
587 + */
588 + object_unref(mon);
589 + return;
590 + }
591 +
592 + user_creatable_del(mon_id, &local_error);
593 + /* Pairs with ref from monitor_qmp_event */
594 + object_unref(mon);
595 + if (local_error != NULL) {
596 + error_report_err(local_error);
597 + } else {
598 + qmp_chardev_remove(chardev_id, NULL);
599 + }
600 +}
601 +
602 static void monitor_qmp_event(void *opaque, QEMUChrEvent event)
603 {
604 QDict *data;
605 MonitorQMP *mon = opaque;
606
607 + /*
608 + * Protect against race if a client drops & quickly
609 + * reconnects - we'll have the delete BH scheduled
610 + * so must not honour a new open request
611 + */
612 + if (mon->delete_pending) {
613 + return;
614 + }
615 +
616 switch (event) {
617 case CHR_EVENT_OPENED:
618 WITH_QEMU_LOCK_GUARD(&mon->parent_obj.mon_lock) {
@@ -577,6 +635,28 @@ static void monitor_qmp_event(void *opaque, QEMUChrEvent event)
635 json_message_parser_init(&mon->parser, handle_qmp_command,
636 mon, NULL);
637 monitor_fdsets_cleanup();
638 + switch (mon->close_action) {
639 + case MONITOR_QMP_CLOSE_ACTION_NONE:
640 + break;
641 + case MONITOR_QMP_CLOSE_ACTION_DELETE:
642 + mon->delete_pending = true;
643 + /*
644 + * Do NOT run in the AIO context associated with the
645 + * monitor. We need to run in the default AIO context
646 + * which is the same context in which 'qmp_object_del'
647 + * will execute
648 + *
649 + * Hold an extra ref in case a separate monitor races
650 + * with the BH by processing an explicit 'object-del'.
651 + * Will be released by monitor_qmp_self_delete_bh
652 + */
653 + object_ref(mon);
654 + aio_bh_schedule_oneshot(qemu_get_aio_context(),
655 + monitor_qmp_self_delete_bh, mon);
656 + break;
657 + default:
658 + g_assert_not_reached();
659 + }
660 break;
661 case CHR_EVENT_BREAK:
662 case CHR_EVENT_MUX_IN:
qapi/qom.json
+21 -1
@@ -1213,6 +1213,22 @@
1213 'base': 'MonitorProperties',
1214 'data': { '*readline': 'bool' } }
1215
1216 +
1217 +##
1218 +# @MonitorQMPCloseAction:
1219 +#
1220 +# Action to take when the character device backend is closed.
1221 +#
1222 +# @none: take no action
1223 +#
1224 +# @delete: delete both the 'monitor-qmp' object and its associated
1225 +# character device backend object
1226 +#
1227 +# Since: 11.1
1228 +##
1229 +{ 'enum': 'MonitorQMPCloseAction',
1230 + 'data': ['none', 'delete'] }
1231 +
1232 ##
1233 # @MonitorQMPProperties:
1234 #
@@ -1220,11 +1236,15 @@
1236 #
1237 # @pretty: whether to pretty print JSON responses (default: false)
1238 #
1239 +# @close-action: action to take when the character device backend is
1240 +# closed (default: none)
1241 +#
1242 # Since: 11.1
1243 ##
1244 { 'struct': 'MonitorQMPProperties',
1245 'base': 'MonitorProperties',
1227 - 'data': { '*pretty': 'bool' } }
1246 + 'data': { '*pretty': 'bool',
1247 + '*close-action': 'MonitorQMPCloseAction' } }
1248
1249 ##
1250 # @ObjectType:
tests/functional/generic/test_monitor_hotplug.py
+79 -4
@@ -25,7 +25,7 @@ class MonitorHotplug(QemuSystemTest):
25 sock_dir = self.socket_dir()
26 self._sock_path = os.path.join(sock_dir.name, 'hotplug.sock')
27
28 - def _add_monitor(self):
28 + def _add_monitor(self, autodelete=False):
29 """Create a chardev + monitor and return the socket path."""
30 sock = self._sock_path
31 self.vm.cmd('chardev-add', id='hotplug-chr', backend={
@@ -39,9 +39,15 @@ class MonitorHotplug(QemuSystemTest):
39 'wait': False,
40 }
41 })
42 - self.vm.cmd('object-add', id='hotplug-mon',
43 - qom_type='monitor-qmp',
44 - chardev='hotplug-chr')
42 + if autodelete:
43 + self.vm.cmd('object-add', id='hotplug-mon',
44 + qom_type='monitor-qmp',
45 + chardev='hotplug-chr',
46 + close_action='delete')
47 + else:
48 + self.vm.cmd('object-add', id='hotplug-mon',
49 + qom_type='monitor-qmp',
50 + chardev='hotplug-chr')
51 return sock
52
53 def _remove_monitor(self):
@@ -118,6 +124,75 @@ class MonitorHotplug(QemuSystemTest):
124 # Clean up the chardev
125 self.vm.cmd('chardev-remove', id='hotplug-chr')
126
127 + def test_auto_delete(self):
128 + """
129 + A dynamically-added monitor configured with 'close-action=delete'
130 + should see itself deleted when the client is closed.
131 + """
132 + self.set_machine('none')
133 + self.vm.add_args('-nodefaults')
134 + self.vm.launch()
135 +
136 + sock = self._add_monitor(autodelete=True)
137 +
138 + cdevs = [c["label"] for c in self.vm.cmd('query-chardev')]
139 + objs = [o["name"] for o in self.vm.cmd('qom-list', path='/objects')]
140 + assert ('hotplug-chr' in cdevs)
141 + assert ('hotplug-mon' in objs)
142 +
143 + qmp = QEMUMonitorProtocol(sock)
144 + greeting = qmp.connect(negotiate=True)
145 + self.assertIn('QMP', greeting)
146 +
147 + cdevs = [c["label"] for c in self.vm.cmd('query-chardev')]
148 + objs = [o["name"] for o in self.vm.cmd('qom-list', path='/objects')]
149 + assert ('hotplug-chr' in cdevs)
150 + assert ('hotplug-mon' in objs)
151 +
152 + qmp.close()
153 +
154 + # Wait upto 10 seconds max for chardev to auto-delete, which
155 + # is hopefully enough for reliability under high load
156 + for i in range(int(10 / 0.2)):
157 + cdevs = [c["label"] for c in self.vm.cmd('query-chardev')]
158 + if 'hotplug-chr' not in cdevs:
159 + break
160 + # Wait a little more then try again
161 + time.sleep(0.2)
162 +
163 + cdevs = [c["label"] for c in self.vm.cmd('query-chardev')]
164 + objs = [o["name"] for o in self.vm.cmd('qom-list', path='/objects')]
165 + assert ('hotplug-chr' not in cdevs)
166 + assert ('hotplug-mon' not in objs)
167 +
168 + def test_reconnect(self):
169 + """
170 + A dynamically-added monitor configured without 'close-action'
171 + should allow reconnects after the client is closed.
172 + """
173 + self.set_machine('none')
174 + self.vm.add_args('-nodefaults')
175 + self.vm.launch()
176 +
177 + sock = self._add_monitor()
178 +
179 + qmp = QEMUMonitorProtocol(sock)
180 + qmp.connect(negotiate=True)
181 +
182 + resp = qmp.cmd_obj({'execute': 'query-chardev'})
183 + self.assertIn('return', resp)
184 +
185 + qmp.close()
186 +
187 + qmp = QEMUMonitorProtocol(sock)
188 + qmp.connect(negotiate=True)
189 +
190 + resp = qmp.cmd_obj({'execute': 'query-chardev'})
191 + self.assertIn('return', resp)
192 +
193 + qmp.close()
194 + self._remove_monitor()
195 +
196 def test_large_response(self):
197 """
198 Send a command with a large response (query-qmp-schema) on a