Ensure the socket listener still exists

This commit is contained in:
Hannah von Reth 2021-02-19 16:00:32 +01:00 committed by Matthieu Gallien
parent 4b0122093a
commit c273c8f71b
5 changed files with 173 additions and 103 deletions

View File

@ -70,8 +70,6 @@
#include <QProcess>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <networkjobs.h>
#ifdef Q_OS_MAC
#include <CoreFoundation/CoreFoundation.h>
@ -197,11 +195,6 @@ Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg)
Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg)
void SocketListener::sendMessage(const QString &function, const QJsonObject &obj, bool doWait) const
{
sendMessage(function + QLatin1Char(':') + QJsonDocument(obj).toJson(QJsonDocument::Compact), doWait);
}
void SocketListener::sendMessage(const QString &message, bool doWait) const
{
if (!socket) {
@ -225,16 +218,6 @@ void SocketListener::sendMessage(const QString &message, bool doWait) const
}
}
struct ListenerHasSocketPred
{
QIODevice *socket;
ListenerHasSocketPred(QIODevice *socket)
: socket(socket)
{
}
bool operator()(const SocketListener &listener) const { return listener.socket == socket; }
};
SocketApi::SocketApi(QObject *parent)
: QObject(parent)
{
@ -242,6 +225,7 @@ SocketApi::SocketApi(QObject *parent)
qRegisterMetaType<SocketListener *>("SocketListener*");
qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
qRegisterMetaType<QSharedPointer<SocketApiJobV2>>("QSharedPointer<SocketApiJobV2>");
if (Utility::isWindows()) {
socketPath = QLatin1String(R"(\\.\pipe\)")
@ -312,7 +296,7 @@ SocketApi::~SocketApi()
qCDebug(lcSocketApi) << "dtor";
_localServer.close();
// All remaining sockets will be destroyed with _localServer, their parent
ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer);
ASSERT(_listeners.isEmpty() || _listeners.first()->socket->parent() == &_localServer)
_listeners.clear();
}
@ -331,14 +315,13 @@ void SocketApi::slotNewConnection()
connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed);
ASSERT(socket->readAll().isEmpty());
_listeners.append(SocketListener(socket));
SocketListener &listener = _listeners.last();
foreach (Folder *f, FolderMan::instance()->map()) {
auto listener = QSharedPointer<SocketListener>::create(socket);
_listeners.insert(socket, listener);
for (Folder *f : FolderMan::instance()->map()) {
if (f->canSync()) {
QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
listener.sendMessage(message);
qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener->socket;
listener->sendMessage(message);
}
}
}
@ -350,13 +333,13 @@ void SocketApi::onLostConnection()
auto socket = qobject_cast<QIODevice *>(sender());
ASSERT(socket);
_listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
_listeners.remove(socket);
}
void SocketApi::slotSocketDestroyed(QObject *obj)
{
auto *socket = static_cast<QIODevice *>(obj);
_listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
_listeners.remove(socket);
}
void SocketApi::slotReadSocket()
@ -370,27 +353,38 @@ void SocketApi::slotReadSocket()
// the readyRead() signals are received - in that case there won't be a
// valid listener. We execute the handler anyway, but it will work with
// a SocketListener that doesn't send any messages.
static auto noListener = SocketListener(nullptr);
SocketListener *listener = &noListener;
auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket));
if (listenerIt != _listeners.end()) {
listener = &*listenerIt;
}
static auto invalidListener = QSharedPointer<SocketListener>::create(nullptr);
const auto listener = _listeners.value(socket, invalidListener);
while (socket->canReadLine()) {
// Make sure to normalize the input from the socket to
// make sure that the path will match, especially on OS X.
const QString line = QString::fromUtf8(socket->readLine().trimmed()).normalized(QString::NormalizationForm_C);
qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket;
const QByteArray command = line.midRef(0, line.indexOf(QLatin1Char(':'))).toUtf8().toUpper().replace("/", "_");
const QByteArray functionWithArguments = "command_" + command + (command.startsWith("ASYNC_") ? "(QSharedPointer<SocketApiJob>)" : "(QString,SocketListener*)");
const int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments);
const int argPos = line.indexOf(QLatin1Char(':'));
const QByteArray command = line.midRef(0, argPos).toUtf8().toUpper();
const int indexOfMethod = [&] {
QByteArray functionWithArguments = QByteArrayLiteral("command_");
if (command.startsWith("ASYNC_")) {
functionWithArguments += command + QByteArrayLiteral("(QSharedPointer<SocketApiJob>)");
} else if (command.startsWith("V2/")) {
functionWithArguments += QByteArrayLiteral("V2_") + command.mid(3) + QByteArrayLiteral("(QSharedPointer<SocketApiJobV2>)");
} else {
functionWithArguments += command + QByteArrayLiteral("(QString,SocketListener*)");
}
Q_ASSERT(staticQtMetaObject.normalizedSignature(functionWithArguments) == functionWithArguments);
const auto out = staticMetaObject.indexOfMethod(functionWithArguments);
if (out == -1) {
listener->sendError(QStringLiteral("Function %1 not found").arg(QString::fromUtf8(functionWithArguments)));
}
ASSERT(out != -1)
return out;
}();
const auto argument = line.midRef(command.length() + 1);
const auto argument = argPos != -1 ? line.midRef(argPos + 1) : QStringRef();
if (command.startsWith("ASYNC_")) {
auto arguments = argument.split('|');
if (arguments.size() != 2) {
listener->sendMessage(QStringLiteral("argument count is wrong"));
listener->sendError(QStringLiteral("argument count is wrong"));
return;
}
@ -409,15 +403,31 @@ void SocketApi::slotReadSocket()
<< "with argument:" << argument;
socketApiJob->reject(QStringLiteral("command not found"));
}
} else if (command.startsWith("V2/")) {
QJsonParseError error;
const auto json = QJsonDocument::fromJson(argument.toUtf8(), &error).object();
if (error.error != QJsonParseError::NoError) {
qCWarning(lcSocketApi()) << "Invalid json" << argument.toString() << error.errorString();
listener->sendError(error.errorString());
return;
}
auto socketApiJob = QSharedPointer<SocketApiJobV2>::create(listener, command, json);
if (indexOfMethod != -1) {
staticMetaObject.method(indexOfMethod)
.invoke(this, Qt::QueuedConnection,
Q_ARG(QSharedPointer<SocketApiJobV2>, socketApiJob));
} else {
qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command
<< "with argument:" << argument;
socketApiJob->failure(QStringLiteral("command not found"));
}
} else {
if (indexOfMethod != -1) {
// to ensure that listener is still valid we need to call it with Qt::DirectConnection
ASSERT(thread() == QThread::currentThread())
staticMetaObject.method(indexOfMethod)
.invoke(this, Qt::DirectConnection, Q_ARG(QString, argument.toString()),
Q_ARG(SocketListener *, listener));
} else {
qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
Q_ARG(SocketListener *, listener.data()));
}
}
}
@ -431,10 +441,10 @@ void SocketApi::slotRegisterPath(const QString &alias)
Folder *f = FolderMan::instance()->folder(alias);
if (f) {
QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
foreach (auto &listener, _listeners) {
qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
listener.sendMessage(message);
const QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
for (const auto &listener : qAsConst(_listeners)) {
qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener->socket;
listener->sendMessage(message);
}
}
@ -479,8 +489,8 @@ void SocketApi::slotUpdateFolderView(Folder *f)
void SocketApi::broadcastMessage(const QString &msg, bool doWait)
{
foreach (auto &listener, _listeners) {
listener.sendMessage(msg, doWait);
for (const auto &listener : qAsConst(_listeners)) {
listener->sendMessage(msg, doWait);
}
}
@ -530,8 +540,8 @@ void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileSt
QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
Q_ASSERT(!systemPath.endsWith('/'));
uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/')));
foreach (auto &listener, _listeners) {
listener.sendMessageIfDirectoryMonitored(msg, directoryHash);
for (const auto &listener : qAsConst(_listeners)) {
listener->sendMessageIfDirectoryMonitored(msg, directoryHash);
}
}
@ -935,20 +945,20 @@ void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
solver.setRemoteVersionFilename(target);
}
void SocketApi::command_V2_LIST_ACCOUNTS(const QString &, SocketListener *listener) const
void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const
{
QJsonArray out;
for (auto acc : AccountManager::instance()->accounts()) {
// TODO: Use uuid once https://github.com/owncloud/client/pull/8397 is merged
out << QJsonObject({ { "name", acc->account()->displayName() }, { "id", acc->account()->id() } });
}
listener->sendMessage(QStringLiteral("V2/ACCOUNTS"), { { "accounts", out } });
job->success({ { "accounts", out } });
}
void SocketApi::command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const
void SocketApi::command_V2_UPLOAD_FILES_FROM(const QSharedPointer<SocketApiJobV2> &job) const
{
auto job = new SocketUploadJob(listener, argument);
job->start();
auto uploadJob = new SocketUploadJob(job);
uploadJob->start();
}
void SocketApi::emailPrivateLink(const QString &link)
@ -1429,6 +1439,46 @@ QString SocketApi::buildRegisterPathMessage(const QString &path)
return message;
}
void SocketApiJob::resolve(const QString &response)
{
_socketListener->sendMessage(QStringLiteral("RESOLVE|") + _jobId + QLatin1Char('|') + response);
}
void SocketApiJob::resolve(const QJsonObject &response)
{
resolve(QJsonDocument { response }.toJson());
}
void SocketApiJob::reject(const QString &response)
{
_socketListener->sendMessage(QStringLiteral("REJECT|") + _jobId + QLatin1Char('|') + response);
}
SocketApiJobV2::SocketApiJobV2(const QSharedPointer<SocketListener> &socketListener, const QByteArray &command, const QJsonObject &arguments)
: _socketListener(socketListener)
, _command(command)
, _jobId(arguments[QStringLiteral("id")].toString())
, _arguments(arguments[QStringLiteral("arguments")].toObject())
{
ASSERT(!_jobId.isEmpty())
}
void SocketApiJobV2::success(const QJsonObject &response) const
{
doFinish(response);
}
void SocketApiJobV2::failure(const QString &error) const
{
doFinish({ { QStringLiteral("error"), error } });
}
void SocketApiJobV2::doFinish(const QJsonObject &obj) const
{
_socketListener->sendMessage(_command + QStringLiteral("_RESULT:") + QJsonDocument({ { QStringLiteral("id"), _jobId }, { QStringLiteral("arguments"), obj } }).toJson(QJsonDocument::Compact));
Q_EMIT finished();
}
} // namespace OCC
#include "socketapi.moc"

View File

@ -40,6 +40,7 @@ class Folder;
class SocketListener;
class DirectEditor;
class SocketApiJob;
class SocketApiJobV2;
Q_DECLARE_LOGGING_CATEGORY(lcSocketApi)
@ -130,8 +131,8 @@ private:
#endif
// External sync
Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QString &argument, SocketListener *listener) const;
Q_INVOKABLE void command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const;
Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const;
Q_INVOKABLE void command_V2_UPLOAD_FILES_FROM(const QSharedPointer<SocketApiJobV2> &job) const;
// Fetch the private link and call targetFun
void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun);
@ -168,7 +169,7 @@ private:
QString buildRegisterPathMessage(const QString &path);
QSet<QString> _registeredAliases;
QList<SocketListener> _listeners;
QMap<QIODevice *, QSharedPointer<SocketListener>> _listeners;
SocketApiServer _localServer;
};
}

View File

@ -62,13 +62,20 @@ class SocketListener
public:
QPointer<QIODevice> socket;
explicit SocketListener(QIODevice *socket)
: socket(socket)
explicit SocketListener(QIODevice *_socket)
: socket(_socket)
{
}
void sendMessage(const QString &message, bool doWait = false) const;
void sendMessage(const QString &function, const QJsonObject &obj, bool doWait = false) const;
void sendWarning(const QString &message, bool doWait = false) const
{
sendMessage(QStringLiteral("WARNING:") + message, doWait);
}
void sendError(const QString &message, bool doWait = false) const
{
sendMessage(QStringLiteral("ERROR:") + message, doWait);
}
void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const
{
@ -110,30 +117,48 @@ class SocketApiJob : public QObject
{
Q_OBJECT
public:
SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments)
explicit SocketApiJob(const QString &jobId, const QSharedPointer<SocketListener> &socketListener, const QJsonObject &arguments)
: _jobId(jobId)
, _socketListener(socketListener)
, _arguments(arguments)
{
}
void resolve(const QString &response = QString())
{
_socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response);
}
void resolve(const QString &response = QString());
void resolve(const QJsonObject &response) { resolve(QJsonDocument { response }.toJson()); }
void resolve(const QJsonObject &response);
const QJsonObject &arguments() { return _arguments; }
void reject(const QString &response)
{
_socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response);
}
void reject(const QString &response);
protected:
QString _jobId;
QSharedPointer<SocketListener> _socketListener;
QJsonObject _arguments;
};
class SocketApiJobV2 : public QObject
{
Q_OBJECT
public:
explicit SocketApiJobV2(const QSharedPointer<SocketListener> &socketListener, const QByteArray &command, const QJsonObject &arguments);
void success(const QJsonObject &response) const;
void failure(const QString &error) const;
const QJsonObject &arguments() const { return _arguments; }
QByteArray command() const { return _command; }
Q_SIGNALS:
void finished() const;
private:
void doFinish(const QJsonObject &obj) const;
QSharedPointer<SocketListener> _socketListener;
const QByteArray _command;
QString _jobId;
SocketListener *_socketListener;
QJsonObject _arguments;
};
}

View File

@ -25,24 +25,30 @@
using namespace OCC;
SocketUploadJob::SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent)
: QObject(parent)
, _listener(listener)
SocketUploadJob::SocketUploadJob(const QSharedPointer<SocketApiJobV2> &job)
: _apiJob(job)
{
const auto args = QJsonDocument::fromJson(argument.toUtf8()).object();
_localPath = args[QLatin1String("localPath")].toString();
_remotePath = args[QLatin1String("remotePath")].toString();
if (!_remotePath.startsWith("/")) {
connect(job.data(), &SocketApiJobV2::finished, this, &SocketUploadJob::deleteLater);
_localPath = _apiJob->arguments()[QLatin1String("localPath")].toString();
_remotePath = _apiJob->arguments()[QLatin1String("remotePath")].toString();
if (!_remotePath.startsWith(QLatin1Char('/'))) {
_remotePath = QLatin1Char('/') + _remotePath;
}
_pattern = args[QLatin1String("pattern")].toString();
_pattern = job->arguments()[QLatin1String("pattern")].toString();
// TODO: use uuid
const auto accname = args[QLatin1String("account")][QLatin1String("name")].toString();
const auto accname = job->arguments()[QLatin1String("account")][QLatin1String("name")].toString();
auto account = AccountManager::instance()->account(accname);
ENFORCE(QFileInfo(_localPath).isAbsolute())
ENFORCE(_tmp.open())
if (!QFileInfo(_localPath).isAbsolute()) {
job->failure(QStringLiteral("Local path must be a an absolute path"));
return;
}
if (!_tmp.open()) {
job->failure(QStringLiteral("Failed to create temporary database"));
return;
}
_db = new SyncJournalDb(_tmp.fileName(), this);
_engine = new SyncEngine(account->account(), _localPath.endsWith(QLatin1Char('/')) ? _localPath : _localPath + QLatin1Char('/'), _remotePath, _db);
@ -54,10 +60,12 @@ SocketUploadJob::SocketUploadJob(OCC::SocketListener *listener, const QString &a
connect(_engine, &OCC::SyncEngine::finished, this, [this](bool ok) {
if (ok) {
finish({});
_apiJob->success({ { "localPath", _localPath }, { "syncedFiles", QJsonArray::fromStringList(_syncedFiles) } });
}
});
connect(_engine, &OCC::SyncEngine::syncError, this, &SocketUploadJob::finish);
connect(_engine, &OCC::SyncEngine::syncError, this, [this](const QString &error, ErrorCategory) {
_apiJob->failure(error);
});
}
void SocketUploadJob::start()
@ -65,7 +73,7 @@ void SocketUploadJob::start()
auto opt = _engine->syncOptions();
opt.setFilePattern(_pattern);
if (!opt.fileRegex().isValid()) {
finish(opt.fileRegex().errorString());
_apiJob->failure(opt.fileRegex().errorString());
return;
}
_engine->setSyncOptions(opt);
@ -75,19 +83,10 @@ void SocketUploadJob::start()
connect(mkdir, &OCC::MkColJob::finishedWithoutError, _engine, &OCC::SyncEngine::startSync);
connect(mkdir, &OCC::MkColJob::finishedWithError, this, [this](QNetworkReply *reply) {
if (reply->error() == 202) {
finish(QStringLiteral("Destination %1 already exists").arg(_remotePath));
_apiJob->failure(QStringLiteral("Destination %1 already exists").arg(_remotePath));
} else {
finish(reply->errorString());
_apiJob->failure(reply->errorString());
}
});
mkdir->start();
}
void SocketUploadJob::finish(const QString &error)
{
if (!_finished) {
_finished = true;
_listener->sendMessage(QStringLiteral("V2/UPLOAD_FILES_RESULT"), { { "localPath", _localPath }, { "error", error }, { "syncedFiles", QJsonArray::fromStringList(_syncedFiles) } });
deleteLater();
}
}

View File

@ -28,14 +28,11 @@ class SocketUploadJob : public QObject
{
Q_OBJECT
public:
SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent = nullptr);
SocketUploadJob(const QSharedPointer<SocketApiJobV2> &job);
void start();
void finish(const QString &error);
private:
SocketListener *_listener;
QSharedPointer<SocketApiJobV2> _apiJob;
QString _localPath;
QString _remotePath;
QString _pattern;
@ -43,7 +40,5 @@ private:
SyncJournalDb *_db;
SyncEngine *_engine;
QStringList _syncedFiles;
bool _finished = false;
};
}