1
+#!/usr/bin/env python3
2
+#
3
+# SPDX-License-Identifier: GPL-2.0-or-later
4
+#
5
+# Functional test for dynamic QMP monitor hotplug
6
+#
7
+# Copyright (c) 2026 Christian Brauner
8
+
9
+import os
10
+
11
+from qemu_test import QemuSystemTest
12
+
13
+from qemu.qmp.legacy import QEMUMonitorProtocol
14
+
15
+
16
+class MonitorHotplug(QemuSystemTest):
17
+
18
+ def setUp(self):
19
+ super().setUp()
20
+ sock_dir = self.socket_dir()
21
+ self._sock_path = os.path.join(sock_dir.name, 'hotplug.sock')
22
+
23
+ def _add_monitor(self):
24
+ """Create a chardev + monitor and return the socket path."""
25
+ sock = self._sock_path
26
+ self.vm.cmd('chardev-add', id='hotplug-chr', backend={
27
+ 'type': 'socket',
28
+ 'data': {
29
+ 'addr': {
30
+ 'type': 'unix',
31
+ 'data': {'path': sock}
32
+ },
33
+ 'server': True,
34
+ 'wait': False,
35
+ }
36
+ })
37
+ self.vm.cmd('object-add', id='hotplug-mon',
38
+ qom_type='monitor-qmp',
39
+ chardev='hotplug-chr')
40
+ return sock
41
+
42
+ def _remove_monitor(self):
43
+ """Remove the monitor + chardev."""
44
+ self.vm.cmd('object-del', id='hotplug-mon')
45
+ self.vm.cmd('chardev-remove', id='hotplug-chr')
46
+
47
+ def _connect_and_handshake(self, sock_path):
48
+ """
49
+ Connect to the dynamic monitor socket, perform the QMP
50
+ greeting and capability negotiation, send a command, then
51
+ disconnect.
52
+ """
53
+ qmp = QEMUMonitorProtocol(sock_path)
54
+
55
+ # connect(negotiate=True) receives the greeting, validates it,
56
+ # and sends qmp_capabilities automatically.
57
+ greeting = qmp.connect(negotiate=True)
58
+ self.assertIn('QMP', greeting)
59
+ self.assertIn('version', greeting['QMP'])
60
+ self.assertIn('capabilities', greeting['QMP'])
61
+
62
+ # Send a real command to prove the session is fully functional
63
+ resp = qmp.cmd_obj({'execute': 'query-version'})
64
+ self.assertIn('return', resp)
65
+ self.assertIn('qemu', resp['return'])
66
+
67
+ qmp.close()
68
+
69
+ def test_hotplug_cycle(self):
70
+ """
71
+ Hotplug a monitor, do the full QMP handshake, unplug it,
72
+ then repeat the whole cycle a second time.
73
+ """
74
+ self.set_machine('none')
75
+ self.vm.add_args('-nodefaults')
76
+ self.vm.launch()
77
+
78
+ # First cycle
79
+ sock = self._add_monitor()
80
+ self._connect_and_handshake(sock)
81
+ self._remove_monitor()
82
+
83
+ # Second cycle -- same ids, same path, must work
84
+ sock = self._add_monitor()
85
+ self._connect_and_handshake(sock)
86
+ self._remove_monitor()
87
+
88
+ def test_self_removal(self):
89
+ """
90
+ A dynamically-added monitor sends object-del targeting
91
+ itself. Verify the request is rejected, but the monitor
92
+ can still be deleted from outside its own context.
93
+ """
94
+ self.set_machine('none')
95
+ self.vm.add_args('-nodefaults')
96
+ self.vm.launch()
97
+
98
+ sock = self._add_monitor()
99
+
100
+ qmp = QEMUMonitorProtocol(sock)
101
+ greeting = qmp.connect(negotiate=True)
102
+ self.assertIn('QMP', greeting)
103
+
104
+ # Self-removal: the dynamic monitor raises error
105
+ resp = qmp.cmd_obj({'execute': 'object-del',
106
+ 'arguments': {'id': 'hotplug-mon'}})
107
+ self.assertIn('error', resp)
108
+
109
+ qmp.close()
110
+
111
+ resp = self.vm.cmd('object-del', id='hotplug-mon')
112
+
113
+ # Clean up the chardev
114
+ self.vm.cmd('chardev-remove', id='hotplug-chr')
115
+
116
+ def test_large_response(self):
117
+ """
118
+ Send a command with a large response (query-qmp-schema) on a
119
+ dynamically-added monitor to exercise the output buffer flush
120
+ path.
121
+ """
122
+ self.set_machine('none')
123
+ self.vm.add_args('-nodefaults')
124
+ self.vm.launch()
125
+
126
+ sock = self._add_monitor()
127
+
128
+ qmp = QEMUMonitorProtocol(sock)
129
+ qmp.connect(negotiate=True)
130
+
131
+ resp = qmp.cmd_obj({'execute': 'query-qmp-schema'})
132
+ self.assertIn('return', resp)
133
+ self.assertIsInstance(resp['return'], list)
134
+ self.assertGreater(len(resp['return']), 0)
135
+
136
+ qmp.close()
137
+ self._remove_monitor()
138
+
139
+ def test_events_after_negotiation(self):
140
+ """
141
+ Verify that QMP events are delivered on a dynamically-added
142
+ monitor after capability negotiation completes.
143
+ """
144
+ self.set_machine('none')
145
+ self.vm.add_args('-nodefaults')
146
+ self.vm.launch()
147
+
148
+ sock = self._add_monitor()
149
+
150
+ qmp = QEMUMonitorProtocol(sock)
151
+ qmp.connect(negotiate=True)
152
+
153
+ # Trigger a STOP event via the main monitor, then read it
154
+ # from the dynamic monitor.
155
+ self.vm.cmd('stop')
156
+ resp = qmp.pull_event(wait=True)
157
+ self.assertEqual(resp['event'], 'STOP')
158
+
159
+ self.vm.cmd('cont')
160
+ resp = qmp.pull_event(wait=True)
161
+ self.assertEqual(resp['event'], 'RESUME')
162
+
163
+ qmp.close()
164
+ self._remove_monitor()
165
+
166
+
167
+if __name__ == '__main__':
168
+ QemuSystemTest.main()