Commit b887f801 authored by Alexander Saoutkin's avatar Alexander Saoutkin Committed by Fabian Vogt

Introduce KIOFuse DBus API

This patch introduces a way to communicate with the KIOFuse process via DBus
that has feature parity with the current "_control" API.
parent c563df93
......@@ -22,7 +22,10 @@ include(ECMQtDeclareLoggingCategory)
find_package(PkgConfig REQUIRED)
find_package(Qt5 ${Qt5_MIN_VERSION} COMPONENTS Core REQUIRED)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS KIO)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
KIO
DBusAddons
)
pkg_check_modules(FUSE3 REQUIRED fuse3)
if(BUILD_TESTING)
......@@ -33,17 +36,22 @@ set(KIOFUSE_SOURCES
main.cpp
kiofusevfs.cpp
kiofusevfs.h
kiofuseservice.cpp
kiofuseservice.h
kiofusenode.h)
ecm_qt_declare_logging_category(KIOFUSE_SOURCES
HEADER debug.h
IDENTIFIER KIOFUSE_LOG
CATEGORY_NAME org.kde.kio.fuse
DEFAULT_SEVERITY Warning)
HEADER debug.h
IDENTIFIER KIOFUSE_LOG
CATEGORY_NAME org.kde.kio.fuse
DEFAULT_SEVERITY Warning)
add_executable(kio-fuse ${KIOFUSE_SOURCES})
target_include_directories(kio-fuse PRIVATE ${FUSE3_INCLUDE_DIRS})
target_compile_definitions(kio-fuse PRIVATE FUSE_USE_VERSION=31 ${FUSE3_CFLAGS_OTHER})
target_link_libraries(kio-fuse PRIVATE Qt5::Core KF5::KIOCore ${FUSE3_LIBRARIES})
install(TARGETS kio-fuse DESTINATION ${KDE_INSTALL_FULL_LIBEXECDIR})
install(FILES kio-fuse-tmpfiles.conf DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/tmpfiles.d)
kdbusaddons_generate_dbus_service_file(kio-fuse org.kde.KIOFuse ${KDE_INSTALL_FULL_LIBEXECDIR})
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
......@@ -72,8 +72,8 @@ node's virtual type method is called or RTTI is queried using dynamic_cast.
During runtime, the tree can look like this:
"" (ino: 1) "_control" (ino: 3)
KIOFuseRootNode -----> KIOFuseControlNode
"" (ino: 1)
KIOFuseRootNode
|
| "smb"
|------> KIOFuseProtocolNode
......
......@@ -9,36 +9,43 @@ To run the tests, run "make test". There is nothing to install (yet).
To install build dependencies on Arch Linux:
pacman -S base-devel fuse3 cmake extra-cmake-modules qt5base kio
pacman -S base-devel fuse3 cmake extra-cmake-modules qt5base kio kdbusaddons
To install build dependencies on openSUSE Tumbleweed:
zypper install extra-cmake-modules 'cmake(KF5KIO)' 'pkgconfig(fuse3)' kio-devel 'cmake(Qt5Test)'
zypper install extra-cmake-modules 'cmake(KF5KIO)' 'pkgconfig(fuse3)'
kio-devel kdbusaddons-devel 'cmake(Qt5Test)' 'cmake(Qt5Dbus)'
To install build dependencies on Ubuntu 19.04:
apt install fuse3 libfuse3-dev build-essential cmake extra-cmake-modules pkg-config libkf5kio-dev
apt install fuse3 libfuse3-dev build-essential cmake extra-cmake-modules
pkg-config libkf5kio-dev libkf5dbusaddons-dev
Running
-------
Create a new directory somewhere, make sure that no daemon is going to clean
up after it (like systemd-tmpfiles in /run/user/...) and run kio-fuse -d $dir.
The "-d" means that it shows debug output and does not daemonize - that makes
it easier to use it at first.
The "-d" means that it shows debug output and does not daemonize - that makes it
easier to use it at first.
In the directory you'll find a new empty file _control which is used to send
commands to kio-fuse. Let's assume you want to make the files at
ftp://user:p[email protected]/directory accessible in your local file system.
To send the corresponding mount command, run
echo "MOUNT ftp://user:[email protected]/directory" >> $dir/_control
In your session bus you'll find a org.kde.KIOFuse service with an interface that
allows one to communicate with the kio-fuse process.
If it failed, kio-fuse wrote the error message returned by kio into your
terminal. If it succeeded, you won't see any output and the hierarchy is
accessible at $dir/ftp/[email protected]/directory.
After your work is done, simply run "fusermount -u $dir" to unmount the URL and
Let's assume you want to make the files at
ftp://user:[email protected]/directory accessible in your local file system.
To send the corresponding mount command, type the following in the command line:
dbus-send --session --print-reply --type=method_call \
--dest=org.kde.KIOFuse \
/org/kde/KIOFuse \
org.kde.KIOFuse.VFS.mountUrl string:ftp://user:[email protected]/directory
If it failed, kio-fuse will reply with an appropriate error message. If it
succeeded, you will get the location that the URL is mounted on as a reply. In
this case it would be $dir/ftp/[email protected]/directory and the directory will be
accessibly at that URL.
After your work is done, simply run "fusermount3 -u $dir" to unmount the URL and
exit kio-fuse.
Have a lot of fun!
# This is a systemd tmpfiles.d configuration file
#
# tmpfiles.d defaults are set to clean /run/user every now and then
# which includes our kio-fuse mount being mounted in /run/user/<id>/kio-fuse-<6-char-random-str>
#
# This file adds an exclusion rule so that user data doesn't get automatically
# cleaned up (i.e. destroyed).
#
# This exclusion file is derived from the following patch:
# https://mail.gnome.org/archives/commits-list/2013-February/msg01994.html
x /run/user/*/kio-fuse-*/
......@@ -48,7 +48,6 @@ public:
LastDirType = RemoteDirNode,
// File types
ControlNode,
RemoteFileNode,
RemoteSymlinkNode,
};
......@@ -107,13 +106,6 @@ Q_SIGNALS:
void gotChildren(int error);
};
class KIOFuseControlNode : public KIOFuseNode {
public:
using KIOFuseNode::KIOFuseNode;
static const NodeType Type = NodeType::ControlNode;
NodeType type() const override { return Type; }
};
class KIOFuseRemoteFileNode : public QObject, public KIOFuseNode {
Q_OBJECT
public:
......
/*
* Copyright 2019 Alexander Saoutkin <[email protected]>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License or any later version accepted by the membership of
* KDE e.V. (or its successor approved by the membership of KDE
* e.V.), which shall act as a proxy defined in Section 14 of
* version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <QDBusConnection>
#include <QStandardPaths>
#include <QDir>
#include "debug.h"
#include "kiofuseservice.h"
#include "kiofusevfs.h"
KIOFuseService::~KIOFuseService()
{
// Make sure the VFS is unmounted before the member destructors run.
// Any access to the mountpoint would deadlock.
kiofusevfs.stop();
}
bool KIOFuseService::start(struct fuse_args &args, QString mountpoint, bool foreground)
{
if(!m_mountpoint.isEmpty())
{
qWarning(KIOFUSE_LOG) << "Refusing to start already running KIOFuseService";
return false;
}
if(mountpoint.isEmpty())
{
const QString runtimeloc = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
if(runtimeloc.isEmpty())
return false;
m_tempDir.emplace(runtimeloc + QStringLiteral("/kio-fuse-XXXXXX"));
if(!m_tempDir.value().isValid())
return false; // Abort if can't mkdir for some reason
m_mountpoint = m_tempDir.value().path();
}
else
// Don't do a mkdir here, we assume that any given mountpoint dir already exists.
m_mountpoint = mountpoint;
if(!kiofusevfs.start(args, m_mountpoint))
return false;
if(foreground)
return registerService();
else
return registerServiceDaemonized();
}
QString KIOFuseService::mountUrl(const QString& remoteUrl, const QDBusMessage& message)
{
message.setDelayedReply(true);
QUrl url = QUrl::fromUserInput(remoteUrl);
kiofusevfs.mountUrl(url, [=] (auto node, int error) {
if(error)
{
QUrl displayUrl = url;
displayUrl.setPassword({}); // Lets not give back passwords in plaintext...
auto errorReply = message.createErrorReply(
QStringLiteral("org.kde.KIOFuse.VFS.Error.CannotMount"),
QStringLiteral("KIOFuse failed to mount %1: %2").arg(displayUrl.toString(), QLatin1String(strerror(error)))
);
QDBusConnection::sessionBus().send(errorReply);
return;
}
QString localPath = {m_mountpoint + kiofusevfs.virtualPath(node)};
QDBusConnection::sessionBus().send(message.createReply() << localPath);
});
return QString();
}
bool KIOFuseService::registerService()
{
return QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KIOFuse"), this, QDBusConnection::ExportAllSlots)
&& QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KIOFuse"));
}
bool KIOFuseService::registerServiceDaemonized()
{
int waiter[2];
int result = 1;
if(pipe(waiter)) {
perror("kiofuse_daemonize: pipe");
return false;
}
/*
* demonize current process by forking it and killing the
* parent. This makes current process as a child of 'init'.
*/
pid_t cpid = fork();
switch(cpid) {
case -1: // fork failed
perror("kiofuse_daemonize: fork");
return false;
default: // Parent
(void) read(waiter[0], &result, sizeof(result));
if(result)
waitpid(cpid, nullptr, 0);
_exit(result);
case 0: // Child
break;
}
result = registerService() ? 0 : 1;
if(setsid() == -1) {
perror("kiofuse_daemonize: setsid");
result = 1;
}
(void) chdir("/");
/* Propagate completion of daemon initialization */
(void) write(waiter[1], &result, sizeof(result));
close(waiter[0]);
close(waiter[1]);
return result == 0;
}
/*
* Copyright 2019 Alexander Saoutkin <[email protected]>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License or any later version accepted by the membership of
* KDE e.V. (or its successor approved by the membership of KDE
* e.V.), which shall act as a proxy defined in Section 14 of
* version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <QObject>
#include <QDBusMessage>
#include <QTemporaryDir>
#include <QStandardPaths>
#include "kiofusevfs.h"
class KIOFuseService : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.KIOFuse.VFS")
public:
virtual ~KIOFuseService();
/** Attempts to register the service and start kiofusevfs. If both succeed,
* returns true, false otherwise. */
bool start(struct fuse_args &args, QString mountpoint, bool foreground);
public Q_SLOTS:
/** Mounts a URL onto the filesystem, and returns the local path back. */
QString mountUrl(const QString &remoteUrl, const QDBusMessage &message);
private:
/** Registers the kio-fuse process as the org.kde.KIOFuse service.
* Returns false if this fails (otherwise you can't communicate with the process), true otherwise.*/
bool registerService();
/** Daemonizes the kio-fuse process, whilst also managing the registration of the org.kde.KIOFuse service.
* Derived from fuse_daemonize() in libfuse. */
bool registerServiceDaemonized();
KIOFuseVFS kiofusevfs;
/** where kiofusevfs is mounted */
QString m_mountpoint;
/** tempdir created if user does not specify mountpoint */
std::optional<QTemporaryDir> m_tempDir;
};
......@@ -135,10 +135,6 @@ KIOFuseVFS::KIOFuseVFS(QObject *parent)
auto deletedRoot = std::make_shared<KIOFuseRootNode>(KIOFuseIno::Invalid, QString(), attr);
insertNode(deletedRoot, KIOFuseIno::DeletedRoot);
auto control = std::make_shared<KIOFuseControlNode>(KIOFuseIno::Root, QStringLiteral("_control"), attr);
insertNode(control, KIOFuseIno::Control);
control->m_stat.st_mode = S_IFREG | 0400;
}
KIOFuseVFS::~KIOFuseVFS()
......@@ -146,17 +142,16 @@ KIOFuseVFS::~KIOFuseVFS()
stop();
}
bool KIOFuseVFS::start(struct fuse_args &args, const char *mountpoint)
bool KIOFuseVFS::start(struct fuse_args &args, const QString& mountpoint)
{
stop();
m_fuseSession = fuse_session_new(&args, &fuse_ll_ops, sizeof(fuse_ll_ops), this);
if(!m_fuseSession)
return false;
if(!setupSignalHandlers()
|| fuse_session_mount(m_fuseSession, mountpoint) != 0)
|| fuse_session_mount(m_fuseSession, mountpoint.toUtf8().data()) != 0)
{
stop();
return false;
......@@ -289,14 +284,6 @@ void KIOFuseVFS::setattr(fuse_req_t req, fuse_ino_t ino, struct stat *attr, int
default:
fuse_reply_err(req, EOPNOTSUPP);
return;
case KIOFuseNode::NodeType::ControlNode:
// Only truncation to 0 supported
if((to_set & FUSE_SET_ATTR_SIZE) == FUSE_SET_ATTR_SIZE && attr->st_size == 0)
replyAttr(req, node);
else
fuse_reply_err(req, EOPNOTSUPP);
return;
case KIOFuseNode::NodeType::RemoteDirNode:
case KIOFuseNode::NodeType::RemoteFileNode:
{
......@@ -704,9 +691,6 @@ void KIOFuseVFS::open(fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi)
return;
}
if(ino == KIOFuseIno::Control)
fi->direct_io = true; // Necessary to get each command directly
node->m_openCount += 1;
if (!(fi->flags & O_NOATIME))
......@@ -907,9 +891,6 @@ void KIOFuseVFS::read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, fu
});
break;
}
case KIOFuseNode::NodeType::ControlNode:
fuse_reply_err(req, EPERM);
break;
}
}
......@@ -936,18 +917,6 @@ void KIOFuseVFS::write(fuse_req_t req, fuse_ino_t ino, const char *buf, size_t s
fuse_reply_err(req, EIO);
return;
case KIOFuseNode::NodeType::ControlNode:
{
// Intentionally ignoring the offset here
QString command = QString::fromUtf8(buf, size);
that->handleControlCommand(command, [=] (int ret) {
if(ret)
fuse_reply_err(req, ret);
else
fuse_reply_write(req, size);
});
return;
}
case KIOFuseNode::NodeType::RemoteFileNode:
{
QByteArray data(buf, size); // Copy data
......@@ -1225,6 +1194,15 @@ QUrl KIOFuseVFS::remoteUrl(const std::shared_ptr<const KIOFuseNode> &node) const
return {};
}
QString KIOFuseVFS::virtualPath(const std::shared_ptr<KIOFuseNode> &node) const
{
QStringList path;
for(const KIOFuseNode *currentNode = node.get(); currentNode != nullptr; currentNode = nodeForIno(currentNode->m_parentIno).get())
path.prepend(currentNode->m_nodeName);
return path.join(QLatin1Char('/'));
}
void KIOFuseVFS::fillStatForFile(struct stat &attr)
{
static uid_t uid = getuid();
......@@ -1686,32 +1664,6 @@ void KIOFuseVFS::mountUrl(QUrl url, std::function<void (const std::shared_ptr<KI
});
}
void KIOFuseVFS::handleControlCommand(QString cmd, std::function<void (int)> callback)
{
int opEnd = cmd.indexOf(QLatin1Char(' '));
if(opEnd < 0)
return callback(EINVAL);
QStringRef op = cmd.midRef(0, opEnd);
// Command "MOUNT <url>"
if(op == QStringLiteral("MOUNT"))
{
QUrl url = QUrl{cmd.midRef(opEnd + 1).trimmed().toString()};
if(url.isValid())
return mountUrl(url, [=](auto node, int error) {
Q_UNUSED(node);
callback(error ? EINVAL : 0);
});
else
return callback(EINVAL);
}
else
{
qWarning(KIOFUSE_LOG) << "Unknown control operation" << op;
return callback(EINVAL);
}
}
QUrl KIOFuseVFS::makeOriginUrl(QUrl url)
{
// Find out whether the base URL needs to start with a /
......
......@@ -44,8 +44,6 @@ enum KIOFuseIno : fuse_ino_t {
/** The inode number of the parent of deleted nodes. */
DeletedRoot,
/** The inode number of the _control file. */
Control,
/** Dynamic allocation by insertNode starts here. */
DynamicStart,
......@@ -54,14 +52,19 @@ enum KIOFuseIno : fuse_ino_t {
class KIOFuseVFS : public QObject
{
Q_OBJECT
public:
explicit KIOFuseVFS(QObject *parent = nullptr);
~KIOFuseVFS();
/** Mounts the filesystem at mountpoint. Returns true on success. */
bool start(fuse_args &args, const char *mountpoint);
bool start(fuse_args &args, const QString& mountpoint);
/** Umounts the filesystem (if necessary) and flushes dirty nodes. */
void stop();
/** Runs KIO::stat on url and adds a node to the tree if successful. Calls the callback at the end. */
void mountUrl(QUrl url, std::function<void(const std::shared_ptr<KIOFuseNode>&, int)> callback);
/** Returns the path upwards until a root node. */
QString virtualPath(const std::shared_ptr<KIOFuseNode> &node) const;
private Q_SLOTS:
void fuseRequestPending();
......@@ -145,10 +148,6 @@ private:
* If writes happen while a flush is sending data, a flush will be retriggered. */
void awaitNodeFlushed(const std::shared_ptr<KIOFuseRemoteFileNode> &node, std::function<void(int error)> callback);
/** Runs KIO::stat on url and adds a node to the tree if successful. Calls the callback at the end. */
void mountUrl(QUrl url, std::function<void(const std::shared_ptr<KIOFuseNode>&, int)> callback);
/** Handles the _control command in cmd asynchronously and call callback upon completion or failure. */
void handleControlCommand(QString cmd, std::function<void(int error)> callback);
/** Returns the override URL for an origin node */
QUrl makeOriginUrl(QUrl url);
......
......@@ -20,10 +20,9 @@
#include <fuse_lowlevel.h>
#include <QSocketNotifier>
#include <QCoreApplication>
#include "kiofusevfs.h"
#include "kiofuseservice.h"
int main(int argc, char *argv[])
{
......@@ -33,12 +32,6 @@ int main(int argc, char *argv[])
if (fuse_parse_cmdline(&args, &opts) != 0)
return 1;
if(opts.mountpoint == nullptr)
{
puts("No mountpoint given.");
opts.show_help = 1;
}
if (opts.show_help)
{
printf("Usage: %s [options] <mountpoint>\n\n", argv[0]);
......@@ -54,11 +47,10 @@ int main(int argc, char *argv[])
}
QCoreApplication a(argc, argv);
KIOFuseVFS kiofusevfs;
if(!kiofusevfs.start(args, opts.mountpoint))
return 1;
KIOFuseService kiofuseservice;
fuse_daemonize(opts.foreground);
if(!kiofuseservice.start(args, QString::fromUtf8(opts.mountpoint), opts.foreground))
return 1;
fuse_opt_free_args(&args);
......
set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
set(KIOFUSE_TEST_SOURCES
fileopstest.cpp)
find_package(Qt5Test CONFIG REQUIRED)
include(ECMAddTests)
find_package(Qt5DBus CONFIG REQUIRED)
ecm_add_test(fileopstest.cpp
TEST_NAME fileopstest
LINK_LIBRARIES Qt5::Test)
qt5_add_dbus_interface(KIOFUSE_TEST_SOURCES org.kde.KIOFuse.VFS.xml kiofuse_interface)
add_executable(fileopstest ${KIOFUSE_TEST_SOURCES})
target_link_libraries(fileopstest PRIVATE Qt5::Test Qt5::DBus)
add_test(NAME fileopstest COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/fileopstest)
set_tests_properties(fileopstest PROPERTIES ENVIRONMENT KDE_FORK_SLAVES=1)
......@@ -28,16 +28,24 @@
#include <QTemporaryDir>
#include <QTemporaryFile>
#include <QTest>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusReply>
#include <QDebug>
#include "kiofuse_interface.h"
class FileOpsTest : public QObject
{
Q_OBJECT
public:
FileOpsTest() : m_kiofuse_iface(QStringLiteral("org.kde.KIOFuse"),
QStringLiteral("/org/kde/KIOFuse"),
QDBusConnection::sessionBus()) {}
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testControlFile();
void testDBusErrorReply();
void testLocalFileOps();
void testLocalDirOps();
void testCreationOps();
......@@ -52,42 +60,31 @@ private Q_SLOTS:
private:
QDateTime roundDownToSecond(QDateTime dt);
QFile m_controlFile;
org::kde::KIOFuse::VFS m_kiofuse_iface;
QTemporaryDir m_mountDir;
};
void FileOpsTest::initTestCase()
{
// QTemporaryDir would otherwise rm -rf on destruction,
// which is fatal if umount fails while something is mounted inside
m_mountDir.setAutoRemove(false);
QString programpath = QFINDTESTDATA("kio-fuse");
QProcess kiofuseProcess;
kiofuseProcess.setProgram(programpath);
kiofuseProcess.setArguments({m_mountDir.path()});
kiofuseProcess.setProcessChannelMode(QProcess::ForwardedChannels);