1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the tools applications of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28
29 #include "qdbusviewer.h"
30 #include "qdbusmodel.h"
31 #include "servicesproxymodel.h"
32 #include "propertydialog.h"
33 #include "logviewer.h"
34
35
36 #include <QtCore/QStringListModel>
37 #include <QtCore/QMetaProperty>
38 #include <QtCore/QSettings>
39 #include <QtGui/QKeyEvent>
40 #include <QtWidgets/QLineEdit>
41 #include <QtWidgets/QAction>
42 #include <QtWidgets/QShortcut>
43 #include <QtWidgets/QVBoxLayout>
44 #include <QtWidgets/QSplitter>
45 #include <QtWidgets/QInputDialog>
46 #include <QtWidgets/QMessageBox>
47 #include <QtWidgets/QMenu>
48 #include <QtWidgets/QTableWidget>
49 #include <QtWidgets/QTreeWidget>
50 #include <QtWidgets/QHeaderView>
51 #include <QtDBus/QDBusConnectionInterface>
52 #include <QtDBus/QDBusInterface>
53 #include <QtDBus/QDBusMetaType>
54
55 #include <private/qdbusutil_p.h>
56
57 class QDBusViewModel: public QDBusModel
58 {
59 public:
QDBusViewModel(const QString & service,const QDBusConnection & connection)60 inline QDBusViewModel(const QString &service, const QDBusConnection &connection)
61 : QDBusModel(service, connection)
62 {}
63
data(const QModelIndex & index,int role=Qt::DisplayRole) const64 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
65 {
66 if (role == Qt::FontRole && itemType(index) == InterfaceItem) {
67 QFont f;
68 f.setItalic(true);
69 return f;
70 }
71 return QDBusModel::data(index, role);
72 }
73 };
74
75 class ServicesModel : public QStringListModel
76 {
77 public:
ServicesModel(QObject * parent=nullptr)78 explicit ServicesModel(QObject *parent = nullptr)
79 : QStringListModel(parent)
80 {}
81
flags(const QModelIndex & index) const82 Qt::ItemFlags flags(const QModelIndex &index) const override
83 {
84 return QStringListModel::flags(index) & ~Qt::ItemIsEditable;
85 }
86 };
87
QDBusViewer(const QDBusConnection & connection,QWidget * parent)88 QDBusViewer::QDBusViewer(const QDBusConnection &connection, QWidget *parent) :
89 QWidget(parent),
90 c(connection),
91 objectPathRegExp(QLatin1String("\\[ObjectPath: (.*)\\]"))
92 {
93 serviceFilterLine = new QLineEdit(this);
94 serviceFilterLine->setPlaceholderText(tr("Search..."));
95
96 // Create model for services list
97 servicesModel = new ServicesModel(this);
98 // Wrap service list model in proxy for easy filtering and interactive sorting
99 servicesProxyModel = new ServicesProxyModel(this);
100 servicesProxyModel->setSourceModel(servicesModel);
101 servicesProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
102
103 servicesView = new QTableView(this);
104 servicesView->installEventFilter(this);
105 servicesView->setModel(servicesProxyModel);
106 // Make services grid view behave like a list view with headers
107 servicesView->verticalHeader()->hide();
108 servicesView->horizontalHeader()->setStretchLastSection(true);
109 servicesView->setShowGrid(false);
110 // Sort service list by default
111 servicesView->setSortingEnabled(true);
112 servicesView->sortByColumn(0, Qt::AscendingOrder);
113
114 connect(serviceFilterLine, &QLineEdit::textChanged, servicesProxyModel, &QSortFilterProxyModel::setFilterFixedString);
115 connect(serviceFilterLine, &QLineEdit::returnPressed, this, &QDBusViewer::serviceFilterReturnPressed);
116
117 tree = new QTreeView;
118 tree->setContextMenuPolicy(Qt::CustomContextMenu);
119
120 connect(tree, &QAbstractItemView::activated, this, &QDBusViewer::activate);
121
122 refreshAction = new QAction(tr("&Refresh"), tree);
123 refreshAction->setData(42); // increase the amount of 42 used as magic number by one
124 refreshAction->setShortcut(QKeySequence::Refresh);
125 connect(refreshAction, &QAction::triggered, this, &QDBusViewer::refreshChildren);
126
127 QShortcut *refreshShortcut = new QShortcut(QKeySequence::Refresh, tree);
128 connect(refreshShortcut, &QShortcut::activated, this, &QDBusViewer::refreshChildren);
129
130 QVBoxLayout *layout = new QVBoxLayout(this);
131 topSplitter = new QSplitter(Qt::Vertical, this);
132 layout->addWidget(topSplitter);
133
134 log = new LogViewer;
135 connect(log, &QTextBrowser::anchorClicked, this, &QDBusViewer::anchorClicked);
136
137 splitter = new QSplitter(topSplitter);
138 splitter->addWidget(servicesView);
139
140 QWidget *servicesWidget = new QWidget;
141 QVBoxLayout *servicesLayout = new QVBoxLayout(servicesWidget);
142 servicesLayout->addWidget(serviceFilterLine);
143 servicesLayout->addWidget(servicesView);
144 splitter->addWidget(servicesWidget);
145 splitter->addWidget(tree);
146
147 topSplitter->addWidget(splitter);
148 topSplitter->addWidget(log);
149
150 connect(servicesView->selectionModel(), &QItemSelectionModel::currentChanged, this, &QDBusViewer::serviceChanged);
151 connect(tree, &QWidget::customContextMenuRequested, this, &QDBusViewer::showContextMenu);
152
153 QMetaObject::invokeMethod(this, "refresh", Qt::QueuedConnection);
154
155 if (c.isConnected()) {
156 logMessage(QLatin1String("Connected to D-Bus."));
157 QDBusConnectionInterface *iface = c.interface();
158 connect(iface, &QDBusConnectionInterface::serviceRegistered, this, &QDBusViewer::serviceRegistered);
159 connect(iface, &QDBusConnectionInterface::serviceUnregistered, this, &QDBusViewer::serviceUnregistered);
160 connect(iface, &QDBusConnectionInterface::serviceOwnerChanged, this, &QDBusViewer::serviceOwnerChanged);
161 } else {
162 logError(QLatin1String("Cannot connect to D-Bus: ") + c.lastError().message());
163 }
164
165 objectPathRegExp.setMinimal(true);
166
167 }
168
topSplitterStateKey()169 static inline QString topSplitterStateKey() { return QStringLiteral("topSplitterState"); }
splitterStateKey()170 static inline QString splitterStateKey() { return QStringLiteral("splitterState"); }
171
saveState(QSettings * settings) const172 void QDBusViewer::saveState(QSettings *settings) const
173 {
174 settings->setValue(topSplitterStateKey(), topSplitter->saveState());
175 settings->setValue(splitterStateKey(), splitter->saveState());
176 }
177
restoreState(const QSettings * settings)178 void QDBusViewer::restoreState(const QSettings *settings)
179 {
180 topSplitter->restoreState(settings->value(topSplitterStateKey()).toByteArray());
181 splitter->restoreState(settings->value(splitterStateKey()).toByteArray());
182 }
183
logMessage(const QString & msg)184 void QDBusViewer::logMessage(const QString &msg)
185 {
186 log->append(msg + QLatin1Char('\n'));
187 }
188
showEvent(QShowEvent *)189 void QDBusViewer::showEvent(QShowEvent *)
190 {
191 serviceFilterLine->setFocus();
192 }
193
eventFilter(QObject * obj,QEvent * event)194 bool QDBusViewer::eventFilter(QObject *obj, QEvent *event)
195 {
196 if (obj == servicesView) {
197 if (event->type() == QEvent::KeyPress) {
198 QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
199 if (keyEvent->modifiers() == Qt::NoModifier) {
200 if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
201 tree->setFocus();
202 }
203 }
204 }
205 }
206 return false;
207 }
208
logError(const QString & msg)209 void QDBusViewer::logError(const QString &msg)
210 {
211 log->append(QLatin1String("<font color=\"red\">Error: </font>") + msg.toHtmlEscaped() + QLatin1String("<br>"));
212 }
213
refresh()214 void QDBusViewer::refresh()
215 {
216 servicesModel->removeRows(0, servicesModel->rowCount());
217
218 if (c.isConnected()) {
219 const QStringList serviceNames = c.interface()->registeredServiceNames();
220 servicesModel->setStringList(serviceNames);
221 }
222 }
223
activate(const QModelIndex & item)224 void QDBusViewer::activate(const QModelIndex &item)
225 {
226 if (!item.isValid())
227 return;
228
229 const QDBusModel *model = static_cast<const QDBusModel *>(item.model());
230
231 BusSignature sig;
232 sig.mService = currentService;
233 sig.mPath = model->dBusPath(item);
234 sig.mInterface = model->dBusInterface(item);
235 sig.mName = model->dBusMethodName(item);
236 sig.mTypeSig = model->dBusTypeSignature(item);
237
238 switch (model->itemType(item)) {
239 case QDBusModel::SignalItem:
240 connectionRequested(sig);
241 break;
242 case QDBusModel::MethodItem:
243 callMethod(sig);
244 break;
245 case QDBusModel::PropertyItem:
246 getProperty(sig);
247 break;
248 default:
249 break;
250 }
251 }
252
getProperty(const BusSignature & sig)253 void QDBusViewer::getProperty(const BusSignature &sig)
254 {
255 QDBusMessage message = QDBusMessage::createMethodCall(sig.mService, sig.mPath, QLatin1String("org.freedesktop.DBus.Properties"), QLatin1String("Get"));
256 QList<QVariant> arguments;
257 arguments << sig.mInterface << sig.mName;
258 message.setArguments(arguments);
259 c.callWithCallback(message, this, SLOT(dumpMessage(QDBusMessage)));
260 }
261
setProperty(const BusSignature & sig)262 void QDBusViewer::setProperty(const BusSignature &sig)
263 {
264 QDBusInterface iface(sig.mService, sig.mPath, sig.mInterface, c);
265 QMetaProperty prop = iface.metaObject()->property(iface.metaObject()->indexOfProperty(sig.mName.toLatin1()));
266
267 bool ok;
268 QString input = QInputDialog::getText(this, tr("Arguments"),
269 tr("Please enter the value of the property %1 (type %2)").arg(
270 sig.mName, QString::fromLatin1(prop.typeName())),
271 QLineEdit::Normal, QString(), &ok);
272 if (!ok)
273 return;
274
275 QVariant value = input;
276 if (!value.convert(prop.type())) {
277 QMessageBox::warning(this, tr("Unable to marshall"),
278 tr("Value conversion failed, unable to set property"));
279 return;
280 }
281
282 QDBusMessage message = QDBusMessage::createMethodCall(sig.mService, sig.mPath, QLatin1String("org.freedesktop.DBus.Properties"), QLatin1String("Set"));
283 QList<QVariant> arguments;
284 arguments << sig.mInterface << sig.mName << QVariant::fromValue(QDBusVariant(value));
285 message.setArguments(arguments);
286 c.callWithCallback(message, this, SLOT(dumpMessage(QDBusMessage)));
287
288 }
289
getDbusSignature(const QMetaMethod & method)290 static QString getDbusSignature(const QMetaMethod& method)
291 {
292 // create a D-Bus type signature from QMetaMethod's parameters
293 QString sig;
294 for (int i = 0; i < method.parameterTypes().count(); ++i) {
295 int type = QMetaType::type(method.parameterTypes().at(i));
296 sig.append(QString::fromLatin1(QDBusMetaType::typeToSignature(type)));
297 }
298 return sig;
299 }
300
callMethod(const BusSignature & sig)301 void QDBusViewer::callMethod(const BusSignature &sig)
302 {
303 QDBusInterface iface(sig.mService, sig.mPath, sig.mInterface, c);
304 const QMetaObject *mo = iface.metaObject();
305
306 // find the method
307 QMetaMethod method;
308 for (int i = 0; i < mo->methodCount(); ++i) {
309 const QString signature = QString::fromLatin1(mo->method(i).methodSignature());
310 if (signature.startsWith(sig.mName) && signature.at(sig.mName.length()) == QLatin1Char('('))
311 if (getDbusSignature(mo->method(i)) == sig.mTypeSig)
312 method = mo->method(i);
313 }
314 if (!method.isValid()) {
315 QMessageBox::warning(this, tr("Unable to find method"),
316 tr("Unable to find method %1 on path %2 in interface %3").arg(
317 sig.mName).arg(sig.mPath).arg(sig.mInterface));
318 return;
319 }
320
321 PropertyDialog dialog;
322 QList<QVariant> args;
323
324 const QList<QByteArray> paramTypes = method.parameterTypes();
325 const QList<QByteArray> paramNames = method.parameterNames();
326 QList<int> types; // remember the low-level D-Bus type
327 for (int i = 0; i < paramTypes.count(); ++i) {
328 const QByteArray paramType = paramTypes.at(i);
329 if (paramType.endsWith('&'))
330 continue; // ignore OUT parameters
331
332 int type = QMetaType::type(paramType);
333 dialog.addProperty(QString::fromLatin1(paramNames.value(i)), type);
334 types.append(type);
335 }
336
337 if (!types.isEmpty()) {
338 dialog.setInfo(tr("Please enter parameters for the method \"%1\"").arg(sig.mName));
339
340 if (dialog.exec() != QDialog::Accepted)
341 return;
342
343 args = dialog.values();
344 }
345
346 // Try to convert the values we got as closely as possible to the
347 // dbus signature. This is especially important for those input as strings
348 for (int i = 0; i < args.count(); ++i) {
349 QVariant a = args.at(i);
350 int desttype = types.at(i);
351 if (desttype < int(QMetaType::User) && desttype != int(QVariant::Map)
352 && a.canConvert(desttype)) {
353 args[i].convert(desttype);
354 }
355 // Special case - convert a value to a QDBusVariant if the
356 // interface wants a variant
357 if (types.at(i) == qMetaTypeId<QDBusVariant>())
358 args[i] = QVariant::fromValue(QDBusVariant(args.at(i)));
359 }
360
361 QDBusMessage message = QDBusMessage::createMethodCall(sig.mService, sig.mPath, sig.mInterface,
362 sig.mName);
363 message.setArguments(args);
364 c.callWithCallback(message, this, SLOT(dumpMessage(QDBusMessage)));
365 }
366
showContextMenu(const QPoint & point)367 void QDBusViewer::showContextMenu(const QPoint &point)
368 {
369 QModelIndex item = tree->indexAt(point);
370 if (!item.isValid())
371 return;
372
373 const QDBusModel *model = static_cast<const QDBusModel *>(item.model());
374
375 BusSignature sig;
376 sig.mService = currentService;
377 sig.mPath = model->dBusPath(item);
378 sig.mInterface = model->dBusInterface(item);
379 sig.mName = model->dBusMethodName(item);
380 sig.mTypeSig = model->dBusTypeSignature(item);
381
382 QMenu menu;
383 menu.addAction(refreshAction);
384
385 switch (model->itemType(item)) {
386 case QDBusModel::SignalItem: {
387 QAction *action = new QAction(tr("&Connect"), &menu);
388 action->setData(1);
389 menu.addAction(action);
390 break; }
391 case QDBusModel::MethodItem: {
392 QAction *action = new QAction(tr("&Call"), &menu);
393 action->setData(2);
394 menu.addAction(action);
395 break; }
396 case QDBusModel::PropertyItem: {
397 QAction *actionSet = new QAction(tr("&Set value"), &menu);
398 actionSet->setData(3);
399 QAction *actionGet = new QAction(tr("&Get value"), &menu);
400 actionGet->setData(4);
401 menu.addAction(actionSet);
402 menu.addAction(actionGet);
403 break; }
404 default:
405 break;
406 }
407
408 QAction *selectedAction = menu.exec(tree->viewport()->mapToGlobal(point));
409 if (!selectedAction)
410 return;
411
412 switch (selectedAction->data().toInt()) {
413 case 1:
414 connectionRequested(sig);
415 break;
416 case 2:
417 callMethod(sig);
418 break;
419 case 3:
420 setProperty(sig);
421 break;
422 case 4:
423 getProperty(sig);
424 break;
425 }
426 }
427
connectionRequested(const BusSignature & sig)428 void QDBusViewer::connectionRequested(const BusSignature &sig)
429 {
430 if (!c.connect(sig.mService, QString(), sig.mInterface, sig.mName, this,
431 SLOT(dumpMessage(QDBusMessage)))) {
432 logError(tr("Unable to connect to service %1, path %2, interface %3, signal %4").arg(
433 sig.mService).arg(sig.mPath).arg(sig.mInterface).arg(sig.mName));
434 }
435 }
436
dumpMessage(const QDBusMessage & message)437 void QDBusViewer::dumpMessage(const QDBusMessage &message)
438 {
439 QList<QVariant> args = message.arguments();
440 QString out = QLatin1String("Received ");
441
442 switch (message.type()) {
443 case QDBusMessage::SignalMessage:
444 out += QLatin1String("signal ");
445 break;
446 case QDBusMessage::ErrorMessage:
447 out += QLatin1String("error message ");
448 break;
449 case QDBusMessage::ReplyMessage:
450 out += QLatin1String("reply ");
451 break;
452 default:
453 out += QLatin1String("message ");
454 break;
455 }
456
457 out += QLatin1String("from ");
458 out += message.service();
459 if (!message.path().isEmpty())
460 out += QLatin1String(", path ") + message.path();
461 if (!message.interface().isEmpty())
462 out += QLatin1String(", interface <i>") + message.interface() + QLatin1String("</i>");
463 if (!message.member().isEmpty())
464 out += QLatin1String(", member ") + message.member();
465 out += QLatin1String("<br>");
466 if (args.isEmpty()) {
467 out += QLatin1String(" (no arguments)");
468 } else {
469 out += QLatin1String(" Arguments: ");
470 for (const QVariant &arg : qAsConst(args)) {
471 QString str = QDBusUtil::argumentToString(arg).toHtmlEscaped();
472 // turn object paths into clickable links
473 str.replace(objectPathRegExp, QLatin1String("[ObjectPath: <a href=\"qdbus://bus\\1\">\\1</a>]"));
474 // convert new lines from command to proper HTML line breaks
475 str.replace(QStringLiteral("\n"), QStringLiteral("<br/>"));
476 out += str;
477 out += QLatin1String(", ");
478 }
479 out.chop(2);
480 }
481
482 log->append(out);
483 }
484
serviceChanged(const QModelIndex & index)485 void QDBusViewer::serviceChanged(const QModelIndex &index)
486 {
487 delete tree->model();
488
489 currentService.clear();
490 if (!index.isValid())
491 return;
492 currentService = index.data().toString();
493
494 QDBusViewModel *model = new QDBusViewModel(currentService, c);
495 tree->setModel(model);
496 connect(model, &QDBusModel::busError, this, &QDBusViewer::logError);
497 }
498
serviceRegistered(const QString & service)499 void QDBusViewer::serviceRegistered(const QString &service)
500 {
501 if (service == c.baseService())
502 return;
503
504 servicesModel->insertRows(0, 1);
505 servicesModel->setData(servicesModel->index(0, 0), service);
506 }
507
findItem(QStringListModel * servicesModel,const QString & name)508 static QModelIndex findItem(QStringListModel *servicesModel, const QString &name)
509 {
510 QModelIndexList hits = servicesModel->match(servicesModel->index(0, 0), Qt::DisplayRole, name);
511 if (hits.isEmpty())
512 return QModelIndex();
513
514 return hits.first();
515 }
516
serviceUnregistered(const QString & name)517 void QDBusViewer::serviceUnregistered(const QString &name)
518 {
519 QModelIndex hit = findItem(servicesModel, name);
520 if (!hit.isValid())
521 return;
522 servicesModel->removeRows(hit.row(), 1);
523 }
524
serviceOwnerChanged(const QString & name,const QString & oldOwner,const QString & newOwner)525 void QDBusViewer::serviceOwnerChanged(const QString &name, const QString &oldOwner,
526 const QString &newOwner)
527 {
528 QModelIndex hit = findItem(servicesModel, name);
529
530 if (!hit.isValid() && oldOwner.isEmpty() && !newOwner.isEmpty())
531 serviceRegistered(name);
532 else if (hit.isValid() && !oldOwner.isEmpty() && newOwner.isEmpty())
533 servicesModel->removeRows(hit.row(), 1);
534 else if (hit.isValid() && !oldOwner.isEmpty() && !newOwner.isEmpty()) {
535 servicesModel->removeRows(hit.row(), 1);
536 serviceRegistered(name);
537 }
538 }
539
serviceFilterReturnPressed()540 void QDBusViewer::serviceFilterReturnPressed()
541 {
542 if (servicesProxyModel->rowCount() <= 0)
543 return;
544
545 servicesView->selectRow(0);
546 servicesView->setFocus();
547 }
548
refreshChildren()549 void QDBusViewer::refreshChildren()
550 {
551 QDBusModel *model = qobject_cast<QDBusModel *>(tree->model());
552 if (!model)
553 return;
554 model->refresh(tree->currentIndex());
555 }
556
anchorClicked(const QUrl & url)557 void QDBusViewer::anchorClicked(const QUrl &url)
558 {
559 if (url.scheme() != QLatin1String("qdbus"))
560 // not ours
561 return;
562
563 // swallow the click without setting a new document
564 log->setSource(QUrl());
565
566 QDBusModel *model = qobject_cast<QDBusModel *>(tree->model());
567 if (!model)
568 return;
569
570 QModelIndex idx = model->findObject(QDBusObjectPath(url.path()));
571 if (!idx.isValid())
572 return;
573
574 tree->scrollTo(idx);
575 tree->setCurrentIndex(idx);
576 }
577