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("&nbsp;&nbsp;(no arguments)");
468     } else {
469         out += QLatin1String("&nbsp;&nbsp;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