1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qqmlnativedebugconnector.h" |
5 | |
6 | #include <private/qhooks_p.h> |
7 | #include <private/qversionedpacket_p.h> |
8 | |
9 | #include <QtQml/qjsengine.h> |
10 | #include <QtCore/qdebug.h> |
11 | #include <QtCore/qjsonarray.h> |
12 | #include <QtCore/qjsondocument.h> |
13 | #include <QtCore/qjsonobject.h> |
14 | #include <QtCore/qjsonvalue.h> |
15 | #include <QtCore/qpointer.h> |
16 | #include <QtCore/qvector.h> |
17 | |
18 | //#define TRACE_PROTOCOL(s) qDebug() << s |
19 | #define TRACE_PROTOCOL(s) |
20 | |
21 | QT_USE_NAMESPACE |
22 | |
23 | static bool expectSyncronousResponse = false; |
24 | Q_GLOBAL_STATIC(QByteArray, responseBuffer) |
25 | |
26 | extern "C" { |
27 | |
28 | Q_DECL_EXPORT const char *qt_qmlDebugMessageBuffer; |
29 | Q_DECL_EXPORT int qt_qmlDebugMessageLength; |
30 | Q_DECL_EXPORT bool qt_qmlDebugConnectionBlocker; |
31 | |
32 | // In blocking mode, this will busy wait until the debugger sets block to false. |
33 | Q_DECL_EXPORT void qt_qmlDebugConnectorOpen(); |
34 | |
35 | // First thing, set the debug stream version. Please use this function as we might move the version |
36 | // member to some other place. |
37 | Q_DECL_EXPORT void qt_qmlDebugSetStreamVersion(int version) |
38 | { |
39 | QQmlNativeDebugConnector::setDataStreamVersion(version); |
40 | } |
41 | |
42 | |
43 | // Break in this one to process output from an asynchronous message/ |
44 | Q_DECL_EXPORT void qt_qmlDebugMessageAvailable() |
45 | { |
46 | } |
47 | |
48 | |
49 | // Break in this one to get notified about construction and destruction of |
50 | // interesting objects, such as QmlEngines. |
51 | Q_DECL_EXPORT void qt_qmlDebugObjectAvailable() |
52 | { |
53 | } |
54 | |
55 | Q_DECL_EXPORT void qt_qmlDebugClearBuffer() |
56 | { |
57 | responseBuffer->clear(); |
58 | qt_qmlDebugMessageBuffer = nullptr; |
59 | qt_qmlDebugMessageLength = 0; |
60 | } |
61 | |
62 | // Send a message to a service. |
63 | Q_DECL_EXPORT bool qt_qmlDebugSendDataToService(const char *serviceName, const char *hexData) |
64 | { |
65 | QByteArray msg = QByteArray::fromHex(hexEncoded: hexData); |
66 | |
67 | QQmlDebugConnector *instance = QQmlDebugConnector::instance(); |
68 | if (!instance) |
69 | return false; |
70 | |
71 | QQmlDebugService *recipient = instance->service(name: serviceName); |
72 | if (!recipient) |
73 | return false; |
74 | |
75 | TRACE_PROTOCOL("Recipient: " << recipient << " got message: " << msg); |
76 | expectSyncronousResponse = true; |
77 | recipient->messageReceived(msg); |
78 | expectSyncronousResponse = false; |
79 | |
80 | return true; |
81 | } |
82 | |
83 | // Enable a service. |
84 | Q_DECL_EXPORT bool qt_qmlDebugEnableService(const char *data) |
85 | { |
86 | QQmlDebugConnector *instance = QQmlDebugConnector::instance(); |
87 | if (!instance) |
88 | return false; |
89 | |
90 | QString name = QString::fromLatin1(ba: data); |
91 | QQmlDebugService *service = instance->service(name); |
92 | if (!service || service->state() == QQmlDebugService::Enabled) |
93 | return false; |
94 | |
95 | service->stateAboutToBeChanged(QQmlDebugService::Enabled); |
96 | service->setState(QQmlDebugService::Enabled); |
97 | service->stateChanged(QQmlDebugService::Enabled); |
98 | return true; |
99 | } |
100 | |
101 | Q_DECL_EXPORT bool qt_qmlDebugDisableService(const char *data) |
102 | { |
103 | QQmlDebugConnector *instance = QQmlDebugConnector::instance(); |
104 | if (!instance) |
105 | return false; |
106 | |
107 | QString name = QString::fromLatin1(ba: data); |
108 | QQmlDebugService *service = instance->service(name); |
109 | if (!service || service->state() == QQmlDebugService::Unavailable) |
110 | return false; |
111 | |
112 | service->stateAboutToBeChanged(QQmlDebugService::Unavailable); |
113 | service->setState(QQmlDebugService::Unavailable); |
114 | service->stateChanged(QQmlDebugService::Unavailable); |
115 | return true; |
116 | } |
117 | |
118 | quintptr qt_qmlDebugTestHooks[] = { |
119 | quintptr(1), // Internal Version |
120 | quintptr(6), // Number of entries following |
121 | quintptr(&qt_qmlDebugMessageBuffer), |
122 | quintptr(&qt_qmlDebugMessageLength), |
123 | quintptr(&qt_qmlDebugSendDataToService), |
124 | quintptr(&qt_qmlDebugEnableService), |
125 | quintptr(&qt_qmlDebugDisableService), |
126 | quintptr(&qt_qmlDebugObjectAvailable), |
127 | quintptr(&qt_qmlDebugClearBuffer) |
128 | }; |
129 | |
130 | // In blocking mode, this will busy wait until the debugger sets block to false. |
131 | Q_DECL_EXPORT void qt_qmlDebugConnectorOpen() |
132 | { |
133 | TRACE_PROTOCOL("Opening native debug connector" ); |
134 | |
135 | // FIXME: Use a dedicated hook. Startup is a safe workaround, though, |
136 | // as we are already beyond its only use. |
137 | qtHookData[QHooks::Startup] = quintptr(&qt_qmlDebugTestHooks); |
138 | |
139 | while (qt_qmlDebugConnectionBlocker) |
140 | ; |
141 | |
142 | TRACE_PROTOCOL("Opened native debug connector" ); |
143 | } |
144 | |
145 | } // extern "C" |
146 | |
147 | QT_BEGIN_NAMESPACE |
148 | |
149 | QQmlNativeDebugConnector::QQmlNativeDebugConnector() |
150 | : m_blockingMode(false) |
151 | { |
152 | const QString args = commandLineArguments(); |
153 | const auto lstjsDebugArguments = QStringView{args}.split(sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts); |
154 | QStringList services; |
155 | for (const QStringView &strArgument : lstjsDebugArguments) { |
156 | if (strArgument == QLatin1String("block" )) { |
157 | m_blockingMode = true; |
158 | } else if (strArgument == QLatin1String("native" )) { |
159 | // Ignore. This is used to signal that this connector |
160 | // should be loaded and that has already happened. |
161 | } else if (strArgument.startsWith(s: QLatin1String("services:" ))) { |
162 | services.append(t: strArgument.mid(pos: 9).toString()); |
163 | } else if (!services.isEmpty()) { |
164 | services.append(t: strArgument.toString()); |
165 | } else if (!strArgument.startsWith(s: QLatin1String("connector:" ))) { |
166 | qWarning(msg: "QML Debugger: Invalid argument \"%s\" detected. Ignoring the same." , |
167 | strArgument.toUtf8().constData()); |
168 | } |
169 | } |
170 | setServices(services); |
171 | } |
172 | |
173 | QQmlNativeDebugConnector::~QQmlNativeDebugConnector() |
174 | { |
175 | for (QQmlDebugService *service : std::as_const(t&: m_services)) { |
176 | service->stateAboutToBeChanged(QQmlDebugService::NotConnected); |
177 | service->setState(QQmlDebugService::NotConnected); |
178 | service->stateChanged(QQmlDebugService::NotConnected); |
179 | } |
180 | } |
181 | |
182 | bool QQmlNativeDebugConnector::blockingMode() const |
183 | { |
184 | return m_blockingMode; |
185 | } |
186 | |
187 | QQmlDebugService *QQmlNativeDebugConnector::service(const QString &name) const |
188 | { |
189 | for (QVector<QQmlDebugService *>::ConstIterator i = m_services.begin(); i != m_services.end(); |
190 | ++i) { |
191 | if ((*i)->name() == name) |
192 | return *i; |
193 | } |
194 | return nullptr; |
195 | } |
196 | |
197 | void QQmlNativeDebugConnector::addEngine(QJSEngine *engine) |
198 | { |
199 | Q_ASSERT(!m_engines.contains(engine)); |
200 | |
201 | TRACE_PROTOCOL("Add engine to connector:" << engine); |
202 | for (QQmlDebugService *service : std::as_const(t&: m_services)) |
203 | service->engineAboutToBeAdded(engine); |
204 | |
205 | announceObjectAvailability(objectType: QLatin1String("qmlengine" ), object: engine, available: true); |
206 | |
207 | for (QQmlDebugService *service : std::as_const(t&: m_services)) |
208 | service->engineAdded(engine); |
209 | |
210 | m_engines.append(t: engine); |
211 | } |
212 | |
213 | void QQmlNativeDebugConnector::removeEngine(QJSEngine *engine) |
214 | { |
215 | Q_ASSERT(m_engines.contains(engine)); |
216 | |
217 | TRACE_PROTOCOL("Remove engine from connector:" << engine); |
218 | for (QQmlDebugService *service : std::as_const(t&: m_services)) |
219 | service->engineAboutToBeRemoved(engine); |
220 | |
221 | announceObjectAvailability(objectType: QLatin1String("qmlengine" ), object: engine, available: false); |
222 | |
223 | for (QQmlDebugService *service : std::as_const(t&: m_services)) |
224 | service->engineRemoved(engine); |
225 | |
226 | m_engines.removeOne(t: engine); |
227 | } |
228 | |
229 | bool QQmlNativeDebugConnector::hasEngine(QJSEngine *engine) const |
230 | { |
231 | return m_engines.contains(t: engine); |
232 | } |
233 | |
234 | void QQmlNativeDebugConnector::announceObjectAvailability(const QString &objectType, |
235 | QObject *object, bool available) |
236 | { |
237 | QJsonObject ob; |
238 | ob.insert(key: QLatin1String("objecttype" ), value: objectType); |
239 | ob.insert(key: QLatin1String("object" ), value: QString::number(quintptr(object))); |
240 | ob.insert(key: QLatin1String("available" ), value: available); |
241 | QJsonDocument doc; |
242 | doc.setObject(ob); |
243 | |
244 | QByteArray ba = doc.toJson(format: QJsonDocument::Compact); |
245 | qt_qmlDebugMessageBuffer = ba.constData(); |
246 | qt_qmlDebugMessageLength = ba.size(); |
247 | TRACE_PROTOCOL("Reporting engine availabilty" ); |
248 | qt_qmlDebugObjectAvailable(); // Trigger native breakpoint. |
249 | qt_qmlDebugMessageBuffer = nullptr; |
250 | qt_qmlDebugMessageLength = 0; |
251 | } |
252 | |
253 | bool QQmlNativeDebugConnector::addService(const QString &name, QQmlDebugService *service) |
254 | { |
255 | TRACE_PROTOCOL("Add service to connector: " << qPrintable(name) << service); |
256 | for (auto it = m_services.cbegin(), end = m_services.cend(); it != end; ++it) { |
257 | if ((*it)->name() == name) |
258 | return false; |
259 | } |
260 | |
261 | connect(sender: service, signal: &QQmlDebugService::messageToClient, |
262 | context: this, slot: &QQmlNativeDebugConnector::sendMessage); |
263 | connect(sender: service, signal: &QQmlDebugService::messagesToClient, |
264 | context: this, slot: &QQmlNativeDebugConnector::sendMessages); |
265 | |
266 | service->setState(QQmlDebugService::Unavailable); |
267 | |
268 | m_services << service; |
269 | return true; |
270 | } |
271 | |
272 | bool QQmlNativeDebugConnector::removeService(const QString &name) |
273 | { |
274 | for (QVector<QQmlDebugService *>::Iterator i = m_services.begin(); i != m_services.end(); ++i) { |
275 | if ((*i)->name() == name) { |
276 | QQmlDebugService *service = *i; |
277 | m_services.erase(pos: i); |
278 | service->setState(QQmlDebugService::NotConnected); |
279 | |
280 | disconnect(sender: service, signal: &QQmlDebugService::messagesToClient, |
281 | receiver: this, slot: &QQmlNativeDebugConnector::sendMessages); |
282 | disconnect(sender: service, signal: &QQmlDebugService::messageToClient, |
283 | receiver: this, slot: &QQmlNativeDebugConnector::sendMessage); |
284 | |
285 | return true; |
286 | } |
287 | } |
288 | return false; |
289 | } |
290 | |
291 | bool QQmlNativeDebugConnector::open(const QVariantHash &configuration) |
292 | { |
293 | m_blockingMode = configuration.value(QStringLiteral("block" ), defaultValue: m_blockingMode).toBool(); |
294 | qt_qmlDebugConnectionBlocker = m_blockingMode; |
295 | qt_qmlDebugConnectorOpen(); |
296 | return true; |
297 | } |
298 | |
299 | void QQmlNativeDebugConnector::setDataStreamVersion(int version) |
300 | { |
301 | Q_ASSERT(version <= QDataStream::Qt_DefaultCompiledVersion); |
302 | s_dataStreamVersion = version; |
303 | } |
304 | |
305 | void QQmlNativeDebugConnector::sendMessage(const QString &name, const QByteArray &message) |
306 | { |
307 | (*responseBuffer) += name.toUtf8() + ' ' + QByteArray::number(message.size()) + ' ' + message; |
308 | qt_qmlDebugMessageBuffer = responseBuffer->constData(); |
309 | qt_qmlDebugMessageLength = responseBuffer->size(); |
310 | // Responses are allowed to accumulate, the buffer will be cleared by |
311 | // separate calls to qt_qmlDebugClearBuffer() once the synchronous |
312 | // function return ('if' branch below) or in the native breakpoint handler |
313 | // ('else' branch below). |
314 | if (expectSyncronousResponse) { |
315 | TRACE_PROTOCOL("Expected synchronous response in " << message); |
316 | // Do not trigger the native breakpoint on qt_qmlDebugMessageFromService. |
317 | } else { |
318 | TRACE_PROTOCOL("Found asynchronous message in " << message); |
319 | // Trigger native breakpoint. |
320 | qt_qmlDebugMessageAvailable(); |
321 | } |
322 | } |
323 | |
324 | void QQmlNativeDebugConnector::sendMessages(const QString &name, const QList<QByteArray> &messages) |
325 | { |
326 | for (int i = 0; i != messages.size(); ++i) |
327 | sendMessage(name, message: messages.at(i)); |
328 | } |
329 | |
330 | QQmlDebugConnector *QQmlNativeDebugConnectorFactory::create(const QString &key) |
331 | { |
332 | return key == QLatin1String("QQmlNativeDebugConnector" ) ? new QQmlNativeDebugConnector : nullptr; |
333 | } |
334 | |
335 | QT_END_NAMESPACE |
336 | |
337 | #include "moc_qqmlnativedebugconnector.cpp" |
338 | |