Akonadi

core/connection.cpp
1 /*
2  * Copyright 2015 Daniel Vrátil <[email protected]>
3  *
4  * This library is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU Library General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or (at your
7  * option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12  * License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB. If not, write to the
16  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  * 02110-1301, USA.
18  */
19 
20 #include "connection_p.h"
21 #include "session_p.h"
22 #include "servermanager_p.h"
23 #include "akonadicore_debug.h"
24 #include "commandbuffer_p.h"
25 #include <private/instance_p.h>
26 
27 #include <QFile>
28 #include <QAbstractEventDispatcher>
29 #include <QFileInfo>
30 #include <QSettings>
31 #include <QApplication>
32 #include <QDateTime>
33 #include <QTimer>
34 
35 #include <private/protocol_exception_p.h>
36 #include <private/standarddirs_p.h>
37 #include <private/datastream_p_p.h>
38 
39 using namespace Akonadi;
40 
41 Connection::Connection(ConnectionType connType, const QByteArray &sessionId,
42  CommandBuffer *commandBuffer, QObject *parent)
43  : QObject(parent)
44  , mConnectionType(connType)
45  , mSessionId(sessionId)
46  , mCommandBuffer(commandBuffer)
47 {
48  qRegisterMetaType<Protocol::CommandPtr>();
49  qRegisterMetaType<QAbstractSocket::SocketState>();
50 
51  const QByteArray sessionLogFile = qgetenv("AKONADI_SESSION_LOGFILE");
52  if (!sessionLogFile.isEmpty()) {
53  mLogFile = new QFile(QStringLiteral("%1.%2.%3.%4-%5").arg(QString::fromLatin1(sessionLogFile))
55  .arg(QString::number(reinterpret_cast<qulonglong>(this), 16),
56  QString::fromLatin1(mSessionId.replace('/', '_')),
57  connType == CommandConnection ? QStringLiteral("Cmd") : QStringLiteral("Ntf")));
58  if (!mLogFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
59  qCWarning(AKONADICORE_LOG) << "Failed to open Akonadi Session log file" << mLogFile->fileName();
60  delete mLogFile;
61  mLogFile = nullptr;
62  }
63  }
64 }
65 
66 Connection::~Connection()
67 {
68  delete mLogFile;
69  if (mSocket) {
70  mSocket->disconnect();
71  mSocket->disconnectFromServer();
72  mSocket->close();
73  mSocket.reset();
74  }
75 }
76 
77 void Connection::reconnect()
78 {
79  const bool ok = QMetaObject::invokeMethod(this, &Connection::doReconnect, Qt::QueuedConnection);
80  Q_ASSERT(ok);
81  Q_UNUSED(ok)
82 }
83 
84 QString Connection::defaultAddressForTypeAndMethod(ConnectionType type, const QString &method)
85 {
86  if (method == QLatin1String("UnixPath")) {
87  const QString defaultSocketDir = StandardDirs::saveDir("data");
88  if (type == CommandConnection) {
89  return defaultSocketDir % QStringLiteral("akonadiserver-cmd.socket");
90  } else if (type == NotificationConnection) {
91  return defaultSocketDir % QStringLiteral("akonadiserver-ntf.socket");
92  }
93  } else if (method == QLatin1String("NamedPipe")) {
94  QString suffix;
95  if (Instance::hasIdentifier()) {
96  suffix += QStringLiteral("%1-").arg(Instance::identifier());
97  }
98  suffix += QString::fromUtf8(QUrl::toPercentEncoding(qApp->applicationDirPath()));
99  if (type == CommandConnection) {
100  return QStringLiteral("Akonadi-Cmd-") % suffix;
101  } else if (type == NotificationConnection) {
102  return QStringLiteral("Akonadi-Ntf-") % suffix;
103  }
104  }
105 
106  Q_UNREACHABLE();
107 }
108 
109 void Connection::doReconnect()
110 {
111  Q_ASSERT(QThread::currentThread() == thread());
112 
113  if (mSocket && (mSocket->state() == QLocalSocket::ConnectedState
114  || mSocket->state() == QLocalSocket::ConnectingState)) {
115  // nothing to do, we are still/already connected
116  return;
117  }
118 
120  return;
121  }
122 
123  // try to figure out where to connect to
124  QString serverAddress;
125 
126  // env var has precedence
127  const QByteArray serverAddressEnvVar = qgetenv("AKONADI_SERVER_ADDRESS");
128  if (!serverAddressEnvVar.isEmpty()) {
129  const int pos = serverAddressEnvVar.indexOf(':');
130  const QByteArray protocol = serverAddressEnvVar.left(pos);
131  QMap<QString, QString> options;
132  const QStringList lst = QString::fromLatin1(serverAddressEnvVar.mid(pos + 1)).split(QLatin1Char(','));
133  for (const QString &entry : lst) {
134  const QStringList pair = entry.split(QLatin1Char('='));
135  if (pair.size() != 2) {
136  continue;
137  }
138  options.insert(pair.first(), pair.last());
139  }
140 
141  if (protocol == "unix") {
142  serverAddress = options.value(QStringLiteral("path"));
143  } else if (protocol == "pipe") {
144  serverAddress = options.value(QStringLiteral("name"));
145  }
146  }
147 
148  // try config file next, fall back to defaults if that fails as well
149  if (serverAddress.isEmpty()) {
150  const QString connectionConfigFile = StandardDirs::connectionConfigFile();
151  const QFileInfo fileInfo(connectionConfigFile);
152  if (!fileInfo.exists()) {
153  qCWarning(AKONADICORE_LOG) << "Akonadi Client Session: connection config file '"
154  "akonadi/akonadiconnectionrc' can not be found!";
155  }
156 
157  QSettings connectionSettings(connectionConfigFile, QSettings::IniFormat);
158 
159  QString connectionType;
160  if (mConnectionType == CommandConnection) {
161  connectionType = QStringLiteral("Data");
162  } else if (mConnectionType == NotificationConnection) {
163  connectionType = QStringLiteral("Notifications");
164  }
165 
166  connectionSettings.beginGroup(connectionType);
167  const auto method = connectionSettings.value(QStringLiteral("Method"), QStringLiteral("UnixPath")).toString();
168  serverAddress = connectionSettings.value(method, defaultAddressForTypeAndMethod(mConnectionType, method)).toString();
169  }
170 
171  mSocket.reset(new QLocalSocket(this));
172 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
173  connect(mSocket.data(), static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), this,
174 #else
175  connect(mSocket.data(), &QLocalSocket::errorOccurred, this,
176 #endif
177  [this](QLocalSocket::LocalSocketError) {
178  qCWarning(AKONADICORE_LOG) << mSocket->errorString() << mSocket->serverName();
179  Q_EMIT socketError(mSocket->errorString());
180  Q_EMIT socketDisconnected();
181  });
182  connect(mSocket.data(), &QLocalSocket::disconnected, this, &Connection::socketDisconnected);
183  // note: we temporarily disconnect from readyRead-signal inside handleIncomingData()
184  connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData);
185 
186  // actually do connect
187  qCDebug(AKONADICORE_LOG) << "connectToServer" << serverAddress;
188  mSocket->connectToServer(serverAddress);
189  if (!mSocket->waitForConnected()) {
190  qCWarning(AKONADICORE_LOG) << "Failed to connect to server!";
191  Q_EMIT socketError(tr("Failed to connect to server!"));
192  mSocket.reset();
193  return;
194  }
195 
196  QTimer::singleShot(0, this, &Connection::handleIncomingData);
197 
198  Q_EMIT reconnected();
199 }
200 
201 void Connection::forceReconnect()
202 {
203  const bool ok = QMetaObject::invokeMethod(this, &Connection::doForceReconnect,
204  Qt::QueuedConnection);
205 
206  Q_ASSERT(ok);
207  Q_UNUSED(ok)
208 }
209 
210 void Connection::doForceReconnect()
211 {
212  Q_ASSERT(QThread::currentThread() == thread());
213 
214  if (mSocket) {
215  mSocket->disconnect(this, SIGNAL(socketDisconnected()));
216  mSocket->disconnectFromServer();
217  mSocket.reset();
218  }
219 }
220 
221 void Connection::closeConnection()
222 {
223  const bool ok = QMetaObject::invokeMethod(this, &Connection::doCloseConnection,
224  Qt::QueuedConnection);
225  Q_ASSERT(ok);
226  Q_UNUSED(ok)
227 }
228 
229 void Connection::doCloseConnection()
230 {
231  Q_ASSERT(QThread::currentThread() == thread());
232 
233  if (mSocket) {
234  mSocket->close();
235  mSocket.reset();
236  }
237 }
238 
239 QLocalSocket *Connection::socket() const
240 {
241  return mSocket.data();
242 }
243 
244 void Connection::handleIncomingData()
245 {
246  Q_ASSERT(QThread::currentThread() == thread());
247 
248  if (!mSocket) { // not connected yet
249  return;
250  }
251 
252  while (mSocket->bytesAvailable() >= int(sizeof(qint64))) {
253  Protocol::DataStream stream(mSocket.data());
254  qint64 tag;
255  stream >> tag;
256 
257  // temporarily disconnect from readyRead-signal to avoid re-entering this function when we
258  // call waitForData() deep inside Protocol::deserialize
259  disconnect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData);
260 
262  try {
263  cmd = Protocol::deserialize(mSocket.data());
264  } catch (const Akonadi::ProtocolException &e) {
265  qCWarning(AKONADICORE_LOG) << "Protocol exception:" << e.what();
266  // cmd's type will be Invalid by default, so fall-through
267  }
268 
269  // reconnect to the signal again
270  connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData);
271 
272  if (!cmd || (cmd->type() == Protocol::Command::Invalid)) {
273  qCWarning(AKONADICORE_LOG) << "Invalid command, the world is going to end!";
274  mSocket->close();
275  reconnect();
276  return;
277  }
278 
279  if (mLogFile) {
280  mLogFile->write("S: ");
281  mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8());
282  mLogFile->write(QByteArray::number(tag));
283  mLogFile->write(" ");
284  mLogFile->write(Protocol::debugString(cmd).toUtf8());
285  mLogFile->write("\n\n");
286  mLogFile->flush();
287  }
288 
289  if (cmd->type() == Protocol::Command::Hello) {
290  Q_ASSERT(cmd->isResponse());
291  }
292 
293  {
294  CommandBufferLocker locker(mCommandBuffer);
295  mCommandBuffer->enqueue(tag, cmd);
296  }
297  }
298 }
299 
300 void Connection::sendCommand(qint64 tag, const Protocol::CommandPtr &cmd)
301 {
302  const bool ok = QMetaObject::invokeMethod(this, "doSendCommand",
303  Qt::QueuedConnection,
304  Q_ARG(qint64, tag),
305  Q_ARG(Akonadi::Protocol::CommandPtr, cmd));
306  Q_ASSERT(ok);
307  Q_UNUSED(ok)
308 }
309 
310 void Connection::doSendCommand(qint64 tag, const Protocol::CommandPtr &cmd)
311 {
312  Q_ASSERT(QThread::currentThread() == thread());
313 
314  if (mLogFile) {
315  mLogFile->write("C: ");
316  mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8());
317  mLogFile->write(QByteArray::number(tag));
318  mLogFile->write(" ");
319  mLogFile->write(Protocol::debugString(cmd).toUtf8());
320  mLogFile->write("\n\n");
321  mLogFile->flush();
322  }
323 
324  if (mSocket && mSocket->isOpen()) {
325  Protocol::DataStream stream(mSocket.data());
326  try {
327  stream << tag;
328  Protocol::serialize(stream, cmd);
329  stream.flush();
330  } catch (const Akonadi::ProtocolException &e) {
331  qCWarning(AKONADICORE_LOG) << "Protocol Exception:" << QString::fromUtf8(e.what());
332  mSocket->close();
333  reconnect();
334  return;
335  }
336  if (!mSocket->waitForBytesWritten()) {
337  qCWarning(AKONADICORE_LOG) << "Socket write timeout";
338  mSocket->close();
339  reconnect();
340  return;
341  }
342  } else {
343  // TODO: Queue the commands and resend on reconnect?
344  }
345 }
virtual QString fileName() const const override
bool isEmpty() const const
static ServerManager * self()
Returns the singleton instance of this class, for connecting to its signals.
QThread * thread() const const
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
QString tr(const char *sourceText, const char *disambiguation, int n)
int size() const const
int indexOf(char ch, int from) const const
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
static State state()
Returns the state of the server.
bool isEmpty() const const
QByteArray number(int n, int base)
QPoint pos() const const
qint64 applicationPid()
T & first()
QByteArray mid(int pos, int len) const const
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
QByteArray left(int len) const const
Server is running and operational.
Definition: servermanager.h:54
QDateTime currentDateTime()
char * toString(const T &value)
QThread * currentThread()
Helper integration between Akonadi and Qt.
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
T & last()
void disconnected()
QByteArray toPercentEncoding(const QString &input, const QByteArray &exclude, const QByteArray &include)
QString fromLatin1(const char *str, int size)
QMap::iterator insert(const Key &key, const T &value)
void readyRead()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Q_EMITQ_EMIT
const T value(const Key &key, const T &defaultValue) const const
QLocalSocket::LocalSocketError error() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Tue May 26 2020 22:46:18 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.