diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cd389c722195cf939bee985e26df2de82caf50f2..67a6480144d3ccebbe39694d528956e96300b3f8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -62,6 +62,9 @@ gkfs:
- sed -i 's/constexpr bool use_dentry_cache = false;/constexpr bool use_dentry_cache = true;/g' "${CI_PROJECT_DIR}/include/config.hpp"
#- sed -i 's/constexpr auto zero_buffer_before_read = false;/constexpr auto zero_buffer_before_read = true;/g' "${CI_PROJECT_DIR}/include/config.hpp"
#- sed -i 's/constexpr auto implicit_data_removal = true;/constexpr auto implicit_data_removal = false;/g' "${CI_PROJECT_DIR}/include/config.hpp"
+ # install libfuse
+ - apt-get update
+ - apt-get install -y libfuse3-dev fuse3
# use ccache
- ccache --zero-stats -M 750MiB -F 800 --evict-older-than 10d
- /usr/sbin/update-ccache-symlinks
@@ -156,7 +159,7 @@ gkfs:integration:
needs: ['gkfs'] # we need to remove gkfs dependencies on manual
parallel:
matrix:
- - SUBTEST: [ data, status, syscalls, directories, operations, position, shell, rename ]
+ - SUBTEST: [ data, status, syscalls, directories, operations, position, shell, rename, fuse ]
rules:
- if: '$CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
when: never
@@ -533,7 +536,7 @@ cppcheck:
script:
- cd ${CI_PROJECT_DIR}
- rm -rf external gkfs
- - cppcheck -I/usr/local/include --xml --enable=warning,style,performance --force ${CI_PROJECT_DIR} 2> cppcheck_out.xml
+ - cppcheck -I/usr/local/include --xml --enable=warning,style,performance --force ${CI_PROJECT_DIR} 2> cppcheck_out.xml
- cppcheck-codequality --input-file cppcheck_out.xml --output-file cppcheck.json
#change paths
after_script:
@@ -544,7 +547,7 @@ cppcheck:
reports:
codequality: cppcheck2.json
expire_in: 5 days
-
+
################################################################################
## Deployment of documentation and reports
diff --git a/CMake/gkfs-options.cmake b/CMake/gkfs-options.cmake
index 8a03d7c48ab985ae55c4ff8ce9d82c1837376f61..b70b99e0761c802e4bc9013d13125e06daea6348 100644
--- a/CMake/gkfs-options.cmake
+++ b/CMake/gkfs-options.cmake
@@ -248,6 +248,13 @@ gkfs_define_option(
DEFAULT_VALUE ON
)
+# use old resolve function
+gkfs_define_option(
+ GKFS_BUILD_FUSE
+ HELP_TEXT "Build FUSE client"
+ DEFAULT_VALUE ON
+)
+
# use old resolve function
gkfs_define_option(
GKFS_USE_LEGACY_PATH_RESOLVE
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 98fc0776b593c1111b6dbe95213c26121f06f4d4..d930172e0b0d8d16fee987613a4948aa98dd10bf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -216,6 +216,11 @@ find_package(Threads REQUIRED)
# details transparently
find_package(Filesystem REQUIRED)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(FUSE3 REQUIRED fuse3)
+include_directories(${FUSE3_INCLUDE_DIRS})
+link_directories(${FUSE3_LIBRARY_DIRS})
+add_definitions(${FUSE3_CFLAGS_OTHER})
# Search for 'source-only' dependencies
###############################################################################
diff --git a/include/client/fuse/fuse_client.hpp b/include/client/fuse/fuse_client.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7c9e255a1262e60a97641e84d3f9d66b0f30850e
--- /dev/null
+++ b/include/client/fuse/fuse_client.hpp
@@ -0,0 +1,125 @@
+/*
+ Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain
+ Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany
+
+ This software was partially supported by the
+ EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu).
+
+ This software was partially supported by the
+ ADA-FS project under the SPPEXA project funded by the DFG.
+
+ This software was partially supported by the
+ the European Union’s Horizon 2020 JTI-EuroHPC research and
+ innovation programme, by the project ADMIRE (Project ID: 956748,
+ admire-eurohpc.eu)
+
+ This project was partially promoted by the Ministry for Digital Transformation
+ and the Civil Service, within the framework of the Recovery,
+ Transformation and Resilience Plan - Funded by the European Union
+ -NextGenerationEU.
+
+ This file is part of GekkoFS' POSIX interface.
+
+ GekkoFS' POSIX interface is free software: you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GekkoFS' POSIX interface 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with GekkoFS' POSIX interface. If not, see
+ .
+
+ SPDX-License-Identifier: LGPL-3.0-or-later
+*/
+
+#ifndef GKFS_CLIENT_FUSE_CONTEXT_HPP
+#define GKFS_CLIENT_FUSE_CONTEXT_HPP
+
+#include "fuse_log.h"
+#include
+extern "C" {
+#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
+#include
+}
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __FreeBSD__
+#include
+#include
+#endif
+
+// GekkoFS Project Headers
+#include
+#include
+#include // Assumed to provide LOG and CTX
+#include
+#include
+#include
+#include
+#include
+
+// TODO do we really need the stat here? no i dont think so
+struct Inode {
+ std::string path;
+ struct stat st;
+ uint64_t lookup_count;
+};
+
+enum {
+ CACHE_NEVER,
+ CACHE_NORMAL,
+ CACHE_ALWAYS,
+};
+
+struct u_data {
+ pthread_mutex_t mutex;
+ int debug;
+ int writeback;
+ int direct_io;
+ int max_readahead;
+ int fifo;
+ int access;
+ int xattr;
+ char* mountpoint;
+ double timeout;
+ int cache;
+ int timeout_set;
+};
+
+struct GkfsDir { // Hypothetical structure that might be used if DIR is cast
+ int fd;
+ long int tell_pos; // for telldir/seekdir
+ char* path;
+ // other members libc DIR might have
+};
+
+#endif // GKFS_CLIENT_FUSE_CONTEXT_HPP
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
index a7d0247f5f6065f16651f2d2cf7affee9d017233..ed6b57004551e7ac38056e67292cdb3ed4295642 100644
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -37,7 +37,7 @@ add_library(gkfs_common SHARED)
target_sources(gkfs_common
PUBLIC
path.cpp
- PRIVATE
+ PRIVATE
logging.cpp
open_file_map.cpp
open_dir.cpp
@@ -104,10 +104,30 @@ target_sources(
hooks.cpp
preload.cpp
preload_context.cpp
- preload_util.cpp
+ preload_util.cpp
)
endif ()
+if (GKFS_BUILD_FUSE)
+add_executable(fuse_client "")
+target_sources(fuse_client
+ PUBLIC
+ ${CMAKE_CURRENT_LIST_DIR}/fuse/fuse_client.cpp
+ PRIVATE
+ ${INCLUDE_DIR}/client/fuse/fuse_client.hpp
+ )
+
+target_link_libraries(fuse_client
+ PRIVATE gkfs_common metadata distributor env_util arithmetic path_util rpc_utils
+ ${FUSE3_LIBRARIES}
+ gkfs_user_lib
+ )
+
+target_include_directories(fuse_client
+ PRIVATE
+ ${FUSE3_INCLUDE_DIRS}
+ )
+endif()
if (GKFS_BUILD_USER_LIB)
target_compile_definitions(gkfs_user_lib PUBLIC BYPASS_SYSCALL ENABLE_USER)
@@ -217,9 +237,13 @@ install(
)
endif ()
+if (GKFS_BUILD_FUSE)
+install(TARGETS fuse_client RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+endif ()
+
install(
TARGETS gkfs_common
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gkfs
-)
\ No newline at end of file
+)
diff --git a/src/client/fuse/fuse_client.cpp b/src/client/fuse/fuse_client.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b9a1197997f4021f2902086b87b39e80a203afc4
--- /dev/null
+++ b/src/client/fuse/fuse_client.cpp
@@ -0,0 +1,1118 @@
+/*
+ Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain
+ Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany
+
+ This software was partially supported by the
+ EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu).
+
+ This software was partially supported by the
+ ADA-FS project under the SPPEXA project funded by the DFG.
+
+ This software was partially supported by the
+ the European Union’s Horizon 2020 JTI-EuroHPC research and
+ innovation programme, by the project ADMIRE (Project ID: 956748,
+ admire-eurohpc.eu)
+
+ This project was partially promoted by the Ministry for Digital Transformation
+ and the Civil Service, within the framework of the Recovery,
+ Transformation and Resilience Plan - Funded by the European Union
+ -NextGenerationEU.
+
+ This file is part of GekkoFS' POSIX interface.
+
+ GekkoFS' POSIX interface is free software: you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GekkoFS' POSIX interface 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with GekkoFS' POSIX interface. If not, see
+ .
+
+ SPDX-License-Identifier: LGPL-3.0-or-later
+*/
+
+#include
+
+static struct fuse_lowlevel_ops ll_ops;
+static std::mutex ino_mutex;
+static std::unordered_map ino_map;
+static std::unordered_map path_map;
+static std::unordered_map local_fifos;
+static fuse_ino_t next_ino = 2; // reserve 1 for root
+static const std::string fifo_path = "/tmp/gekkofs_fifos/";
+
+#ifdef GKFS_DEBUG_BUILD
+#define DEBUG_INFO(ud, fmt, ...) \
+ do { \
+ if((ud) && (ud)->debug) { \
+ fuse_log(FUSE_LOG_DEBUG, "[DEBUG] " fmt "\n", ##__VA_ARGS__); \
+ } \
+ } while(0)
+#else
+#define DEBUG_INFO(...) /* No debug output */
+#endif
+
+static fuse_ino_t
+alloc_inode(const std::string& path) {
+ std::lock_guard lk(ino_mutex);
+ fuse_ino_t ino = next_ino++;
+ ino_map[ino] = {path, {}, 1};
+ path_map[path] = ino;
+ return ino;
+}
+
+static Inode*
+get_inode(fuse_ino_t ino) {
+ std::lock_guard lk(ino_mutex);
+ auto it = ino_map.find(ino);
+ return it != ino_map.end() ? &it->second : nullptr;
+}
+
+static struct u_data*
+udata(fuse_req_t req) {
+ return (struct u_data*) fuse_req_userdata(req);
+}
+
+static std::string
+get_path(const Inode* inode, const char* name) {
+ if(name[0] == '/') {
+ return std::string(name);
+ }
+ if(inode->path == "/") {
+ return inode->path + name;
+ } else {
+ return inode->path + "/" + name;
+ }
+}
+
+static const struct fuse_opt lo_opts[] = {
+ {"writeback", offsetof(struct u_data, writeback), 1},
+ {"no_writeback", offsetof(struct u_data, writeback), 0},
+ {"direct_io", offsetof(struct u_data, direct_io), 1},
+ {"no_direct_io", offsetof(struct u_data, direct_io), 0},
+ {"max_readahead=%ui", offsetof(struct u_data, max_readahead), 0},
+ {"fifo", offsetof(struct u_data, fifo), 1},
+ {"no_fifo", offsetof(struct u_data, fifo), 0},
+ {"access", offsetof(struct u_data, access), 1},
+ {"no_access", offsetof(struct u_data, access), 0},
+ {"xattr", offsetof(struct u_data, xattr), 1},
+ {"no_xattr", offsetof(struct u_data, xattr), 0},
+ {"timeout=%lf", offsetof(struct u_data, timeout), 0},
+ {"timeout=", offsetof(struct u_data, timeout_set), 1},
+ {"cache=never", offsetof(struct u_data, cache), CACHE_NEVER},
+ {"cache=auto", offsetof(struct u_data, cache), CACHE_NORMAL},
+ {"cache=always", offsetof(struct u_data, cache), CACHE_ALWAYS},
+
+ FUSE_OPT_END};
+
+static void
+passthrough_ll_help(void) {
+ printf(" -o writeback Enable writeback\n"
+ " -o no_writeback Disable write back\n"
+ " -o direct_io Enables direct io\n"
+ " -o no_direct_io Disable direct io\n"
+ " -o max_readahead=1 Amount of allowed readaheads\n"
+ " -o fifo Enable fifo, disables GekkoFS access check\n"
+ " -o no_fifo Disable fifo\n"
+ " -o access Enable GekkoFS access check if fifo is deactivated\n"
+ " -o no_access Disable access handler\n"
+ " -o xattr Enable xattr\n"
+ " -o no_xattr Disable xattr\n"
+ " -o timeout=1.0 Caching timeout\n"
+ " -o timeout=0/1 Timeout is set\n"
+ " -o cache=never Disable cache\n"
+ " -o cache=auto Auto enable cache\n"
+ " -o cache=always Cache always\n");
+}
+
+static void
+init_handler(void* userdata, struct fuse_conn_info* conn) {
+ struct u_data* ud = (struct u_data*) userdata;
+ DEBUG_INFO(ud, "init handler readahead %i direct_io %i", ud->max_readahead,
+ ud->direct_io);
+
+ // TODO check other capabilities e.g. FUSE_CAP_READDIRPLUS
+ if(ud->writeback) {
+#if FUSE_MAJOR_VERSION > 3 || \
+ (FUSE_MAJOR_VERSION == 3 && FUSE_MINOR_VERSION >= 12)
+ fuse_set_feature_flag(conn, FUSE_CAP_WRITEBACK_CACHE);
+#else
+ // for older fuse versions like on the ubuntu22
+ conn->want |= FUSE_CAP_WRITEBACK_CACHE;
+#endif
+ DEBUG_INFO(ud, "init_handler: try to activate writeback",
+ ud->max_readahead, ud->direct_io);
+ }
+ // if(lo->flock && conn->capable & FUSE_CAP_FLOCK_LOCKS) {
+ // has_flag = fuse_set_feature_flag(conn, FUSE_CAP_FLOCK_LOCKS);
+ // if(lo->debug && has_flag)
+ // fuse_log(FUSE_LOG_DEBUG, "init_handler: activating flock
+ // locks\n");
+ // }
+
+ /* Disable the receiving and processing of FUSE_INTERRUPT requests */
+ // conn->no_interrupt = 1;
+ conn->max_readahead = ud->max_readahead;
+}
+
+static void
+destroy_handler(void* userdata) {
+ struct u_data* ud = (struct u_data*) userdata;
+ DEBUG_INFO(ud, "destroy handler");
+ // userdata is GekkoFuse* if passed
+}
+
+static void
+lookup_handler(fuse_req_t req, fuse_ino_t parent, const char* name) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "lookup handler ino %u", parent);
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string child = get_path(parent_inode, name);
+ DEBUG_INFO(ud, "lookup %s", child.c_str());
+
+ if(ud->fifo) {
+ auto iit = local_fifos.find(child);
+ if(iit != local_fifos.end()) {
+ const struct stat& st = iit->second;
+
+ fuse_entry_param e{};
+ e.ino = st.st_ino;
+ e.attr = st;
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+
+ fuse_reply_entry(req, &e);
+ return;
+ }
+ }
+
+ // See if we already have this path
+ auto it = path_map.find(child);
+ fuse_ino_t ino;
+ if(it != path_map.end()) {
+ ino = it->second;
+ ino_map[ino].lookup_count++;
+ } else {
+ ino = alloc_inode(child);
+ }
+
+ struct stat st;
+ int rc = gkfs::syscall::gkfs_stat(child, &st);
+ if(rc < 0) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ ino_map[ino].st = st;
+ fuse_entry_param e = {};
+ e.ino = ino;
+ e.attr = st;
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+ fuse_reply_entry(req, &e);
+}
+
+static void
+getattr_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "getattr handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ // query GekkoFS for latest attr
+ struct stat st;
+ int rc = gkfs::syscall::gkfs_stat(inode->path, &st);
+ if(rc) {
+ DEBUG_INFO(ud, "getattr error %u", rc);
+ fuse_reply_err(req, errno);
+ return;
+ }
+ inode->st = st;
+ fuse_reply_attr(req, &st, ud->timeout);
+}
+
+static void
+setattr_handler(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set,
+ struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "setattr handler ino %u", ino);
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+
+ if(to_set & FUSE_SET_ATTR_SIZE) {
+ off_t new_size = attr->st_size;
+ int res = gkfs::syscall::gkfs_truncate(inode->path, new_size);
+ if(res < 0) {
+ DEBUG_INFO(ud, "setattr truncate failed on %s",
+ inode->path.c_str());
+ fuse_reply_err(req, EIO);
+ return;
+ }
+ // Update cached stat so users see the new size
+ inode->st.st_size = new_size;
+ }
+
+ if(to_set & FUSE_SET_ATTR_ATIME)
+ inode->st.st_atim = attr->st_atim;
+ if(to_set & FUSE_SET_ATTR_MTIME)
+ inode->st.st_mtim = attr->st_mtim;
+
+ // TODO because we cannot save the attributes in gekko, we just return the
+ // buffered results of stat
+ fuse_reply_attr(req, &inode->st, ud->timeout);
+ return;
+}
+
+static void
+open_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "open handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ const int mode = 0644; // -rw-r--r-- I think that doesnt matter anyway
+ int fd = gkfs::syscall::gkfs_open(inode->path, mode,
+ fi->flags); // TODO mode!
+ if(fd < 0) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ fi->fh = fd; // TODO file handle == file descriptor?
+ fi->direct_io = ud->direct_io;
+ fuse_reply_open(req, fi);
+}
+
+static void
+lseek_handler(fuse_req_t req, fuse_ino_t ino, off_t off, int whence,
+ struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "lseek handler");
+ int lc = gkfs::syscall::gkfs_lseek(fi->fh, off, whence);
+ if(lc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_reply_lseek(req, lc);
+}
+
+static void
+read_handler(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
+ struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "read handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::vector buf(size);
+ int rc = gkfs::syscall::gkfs_pread(fi->fh, buf.data(), size, off);
+ if(rc < 0) {
+ DEBUG_INFO(ud, "read fail");
+ fuse_reply_err(req, errno);
+ return;
+ }
+ fuse_reply_buf(req, buf.data(), rc);
+}
+
+static void
+write_handler(fuse_req_t req, fuse_ino_t ino, const char* buf, size_t size,
+ off_t off, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "write handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ int rc = gkfs::syscall::gkfs_pwrite(fi->fh, buf, size, off);
+ if(rc < 0) {
+ DEBUG_INFO(ud, "write fail");
+ fuse_reply_err(req, errno);
+ return;
+ }
+ fuse_reply_write(req, rc);
+}
+
+static void
+create_handler(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode,
+ struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string path = get_path(parent_inode, name);
+ DEBUG_INFO(ud, "create handler %s", path.c_str());
+ int fd = gkfs::syscall::gkfs_open(path, mode, fi->flags | O_CREAT);
+ if(fd < 0) {
+ DEBUG_INFO(ud, "create -> open failed errno %i", errno);
+ fuse_reply_err(req, errno);
+ return;
+ }
+ fi->fh = fd;
+ fi->direct_io = ud->direct_io;
+ struct stat st;
+ int sc = gkfs::syscall::gkfs_stat(path, &st);
+ if(sc == -1) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ fuse_ino_t ino = alloc_inode(path);
+ DEBUG_INFO(ud, "create new inode ino %i", ino);
+ ino_map[ino].st = st;
+ fuse_entry_param e = {};
+ e.ino = ino;
+ e.attr = st;
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+ fuse_reply_create(req, &e, fi);
+}
+
+/// TODO normally, the file should only be removed if the lookup count is zero,
+/// problem?
+static void
+unlink_handler(fuse_req_t req, fuse_ino_t parent, const char* name) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "unlink handler");
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string path = get_path(parent_inode, name);
+
+ if(ud->fifo) {
+ auto it = local_fifos.find(path);
+ if(it != local_fifos.end()) {
+ std::string real = fifo_path + std::string(name);
+ unlink(real.c_str());
+ local_fifos.erase(it);
+ fuse_reply_err(req, 0);
+ return;
+ }
+ }
+
+ int rc = gkfs::syscall::gkfs_remove(path);
+ auto it_src = path_map.find(path);
+ if(it_src != path_map.end()) {
+ path_map.erase(it_src);
+ ino_map.erase(it_src->second);
+ }
+ if(rc == -1) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_reply_err(req, 0);
+}
+
+static void
+opendir_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "opendir handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOTDIR);
+ return;
+ }
+ DEBUG_INFO(ud, "open dir %s", inode->path.c_str());
+
+ const int fd = gkfs::syscall::gkfs_opendir(inode->path);
+
+ DEBUG_INFO(ud, "\t with fd %i \n", fd);
+
+ if(fd < 0) {
+ fuse_reply_err(req, ENOTDIR);
+ return;
+ }
+
+ fi->fh = (uint64_t) fd;
+ fuse_reply_open(req, fi);
+}
+
+static void
+readdir_handler(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
+ struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "readdir handler");
+
+ auto open_dir = CTX->file_map()->get_dir(fi->fh);
+
+ DEBUG_INFO(ud, "read dir %s", open_dir->path().c_str());
+
+ if(open_dir == nullptr) {
+ fuse_reply_err(req, EBADF);
+ return;
+ }
+
+ // Allocate a buffer to accumulate entries
+ char* buf = static_cast(malloc(size));
+ if(!buf) {
+ fuse_reply_err(req, ENOMEM);
+ return;
+ }
+
+ size_t bytes_filled = 0;
+ size_t pos = off;
+
+ while(pos < open_dir->size()) {
+ auto de = open_dir->getdent(pos);
+
+ struct stat st{};
+ st.st_ino =
+ std::hash()(open_dir->path() + "/" + de.name());
+ st.st_mode = (de.type() == gkfs::filemap::FileType::regular) ? S_IFREG
+ : S_IFDIR;
+
+ size_t entry_size =
+ fuse_add_direntry(req, buf + bytes_filled, size - bytes_filled,
+ de.name().c_str(), &st, pos + 1);
+
+ if(entry_size > size - bytes_filled)
+ break; // not enough space left
+
+ bytes_filled += entry_size;
+ pos += 1;
+ }
+
+ if(ud->fifo) {
+ int i = 0;
+ for(auto const& kv : local_fifos) {
+ if(i < off) {
+ i++;
+ continue;
+ }
+ std::filesystem::path vpath = kv.first;
+ const struct stat& st = kv.second;
+
+ // check if FIFO belongs to this directory
+ if(vpath.parent_path() != open_dir->path())
+ continue;
+
+ std::string fname = vpath.filename();
+
+ size_t entry_size = fuse_add_direntry(req, buf + bytes_filled,
+ size - bytes_filled,
+ fname.c_str(), &st, pos + 1);
+
+ if(entry_size > size - bytes_filled)
+ break;
+
+ bytes_filled += entry_size;
+ pos += 1;
+ }
+ }
+
+ open_dir->pos(pos); // update internal position if needed
+
+ fuse_reply_buf(req, buf, bytes_filled);
+ free(buf);
+}
+
+static void
+releasedir_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "releasedir handler");
+ if(CTX->interception_enabled() && CTX->file_map()->exist(fi->fh)) {
+ int ret = gkfs::syscall::gkfs_close(fd); // Close GekkoFS internal FD
+
+ fuse_reply_err(req, ret);
+ return;
+ }
+ fuse_reply_err(req, 0);
+}
+
+/// releases file descriptor, not connected to lookup_count
+static void
+release_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "release handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ int lc = gkfs::syscall::gkfs_close(fi->fh);
+ if(lc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_reply_err(req, 0);
+}
+
+/// decrement lookup count
+static void
+forget_handler(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "forget handler");
+
+ auto it = ino_map.find(ino);
+ if(it == ino_map.end()) {
+ fuse_reply_none(req);
+ return;
+ }
+
+ Inode& inode = it->second;
+ if(inode.lookup_count > nlookup)
+ inode.lookup_count -= nlookup;
+ else
+ inode.lookup_count = 0;
+
+ if(inode.lookup_count == 0) { // && inode.open_count == 0
+ path_map.erase(inode.path);
+ ino_map.erase(it);
+ DEBUG_INFO(ud, "reached lookup_count 0");
+ }
+
+ fuse_reply_none(req);
+ // fuse_reply_err(req, 0);
+}
+
+static void
+flush_handler(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "flush handler");
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ int lc = gkfs::syscall::gkfs_fsync(fi->fh);
+ if(lc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_reply_err(req, 0);
+}
+
+static void
+fsync_handler(fuse_req_t req, fuse_ino_t ino, int datasync,
+ struct fuse_file_info* fi) {
+ // on datasync > 0, metadata should not be flushed
+ flush_handler(req, ino, fi);
+}
+
+static void
+access_handler(fuse_req_t req, fuse_ino_t ino, int mask) {
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "access handler");
+ if(ud->access && !ud->fifo) {
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ int lc = gkfs::syscall::gkfs_access(inode->path, mask, true);
+ if(lc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_reply_err(req, 0);
+ } else {
+ // deactivates following access requests and it is always treated as
+ // success
+ fuse_reply_err(req, ENOSYS);
+ }
+}
+
+static void
+mkdir_handler(fuse_req_t req, fuse_ino_t parent, const char* name,
+ mode_t mode) {
+ auto* ud = udata(req);
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string path = get_path(parent_inode, name);
+ DEBUG_INFO(ud, "mkdir parent %s name %s", parent_inode->path.c_str(), name);
+ int rc = gkfs::syscall::gkfs_create(path, mode | S_IFDIR);
+ if(rc == -1) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ struct stat st;
+ int sc = gkfs::syscall::gkfs_stat(path, &st);
+ if(sc == -1) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ fuse_ino_t ino = alloc_inode(path);
+ DEBUG_INFO(ud, "create new inode ino %i", ino);
+ ino_map[ino].st = st;
+ fuse_entry_param e = {};
+ e.ino = ino;
+ e.attr = st;
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+ fuse_reply_entry(req, &e);
+}
+
+static void
+rmdir_handler(fuse_req_t req, fuse_ino_t parent, const char* name) {
+ auto* ud = udata(req);
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string path = get_path(parent_inode, name);
+ DEBUG_INFO(ud, "rmdir %s", path.c_str());
+ int rc = gkfs::syscall::gkfs_rmdir(path);
+ if(rc == -1) {
+ fuse_reply_err(req, errno);
+ return;
+ }
+ auto it_src = path_map.find(path);
+ if(it_src != path_map.end()) {
+ path_map.erase(it_src);
+ ino_map.erase(it_src->second);
+ }
+ fuse_reply_err(req, 0);
+}
+
+static void
+readlink_handler(fuse_req_t req, fuse_ino_t ino) {
+#ifdef HAS_SYMLINKS
+ auto* inode = get_inode(ino);
+ if(!inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ char link[PATH_MAX];
+ int rc = gkfs::syscall::gkfs_readlink(inode->path, link, PATH_MAX);
+ if(rc == -1) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+ link[rc] = '\0';
+ fuse_reply_readlink(req, link);
+#else
+ fuse_reply_err(req, ENOTSUP);
+#endif
+}
+
+static void
+symlink_handler(fuse_req_t req, const char* linkname, fuse_ino_t parent,
+ const char* name) {
+#ifdef HAS_SYMLINKS
+ auto* ud = udata(req);
+ DEBUG_INFO(ud, "symlink handler linkname %s name %s", linkname, name);
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ DEBUG_INFO(ud, "symlink parent inode ino %i", parent);
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+
+ std::string path = get_path(parent_inode, name);
+ std::string target = get_path(parent_inode, linkname);
+
+ if(target.rfind(ud->mountpoint, 0) == 0) {
+ // starts with mount path
+ target = get_path(parent_inode,
+ target.substr(strlen(ud->mountpoint)).c_str());
+ }
+
+ DEBUG_INFO(ud, "mk symlink path %s target %s", path.c_str());
+ int rc = gkfs::syscall::gkfs_mk_symlink(path, target);
+ if(rc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+
+ // Stat the new symlink so we can reply with entry info
+ struct stat st;
+ if(gkfs::syscall::gkfs_stat(path, &st) < 0) {
+ DEBUG_INFO(ud, "stat failed %i", errno);
+ fuse_reply_err(req, errno);
+ return;
+ }
+ DEBUG_INFO(ud, "stat mode %i, iflink %i", st.st_mode, S_IFLNK);
+ // TODO this meta is not saved and therefore on restart gone
+ // this shows the link on ls -l
+ st.st_mode = S_IFLNK | 0777; // mark as symlink + full perms
+ fuse_entry_param e = {};
+ e.ino = alloc_inode(path);
+ e.attr = st;
+ st.st_size = strlen(target.c_str());
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+ fuse_reply_entry(req, &e);
+#else
+ fuse_reply_err(req, ENOTSUP);
+#endif
+}
+
+static void
+rename_handler(fuse_req_t req, fuse_ino_t old_parent, const char* old_name,
+ fuse_ino_t new_parent, const char* new_name,
+ unsigned int flags) {
+
+#ifdef HAS_RENAME
+ auto* old_parent_inode = get_inode(old_parent);
+ if(!old_parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ auto* new_parent_inode = get_inode(new_parent);
+ if(!new_parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+ std::string old_path = get_path(old_parent_inode, old_name);
+ std::string new_path = get_path(new_parent_inode, new_name);
+
+ struct stat st_src{};
+ if(gkfs::syscall::gkfs_stat(old_path, &st_src) < 0) {
+ fuse_reply_err(req, errno == 0 ? ENOENT : errno);
+ return;
+ }
+
+ struct stat st_dst{};
+ const bool dst_exists = (gkfs::syscall::gkfs_stat(new_path, &st_dst) == 0);
+
+ if((flags & RENAME_NOREPLACE) && dst_exists) {
+ fuse_reply_err(req, EEXIST);
+ return;
+ }
+
+ if(flags & RENAME_EXCHANGE) {
+ fuse_reply_err(req, EOPNOTSUPP);
+ return;
+ }
+
+ int rc = gkfs::syscall::gkfs_rename(old_path, new_path);
+ if(rc < 0) {
+ fuse_reply_err(req, 1);
+ return;
+ }
+
+ fuse_ino_t src_ino = 0;
+ auto it_src = path_map.find(old_path);
+ if(it_src != path_map.end()) {
+ src_ino = it_src->second;
+ path_map.erase(it_src);
+ path_map[new_path] = src_ino;
+ ino_map[src_ino].path = new_path;
+ } else {
+ src_ino = alloc_inode(new_path);
+ path_map[new_path] = src_ino;
+ ino_map[src_ino].path = new_path;
+ ino_map[src_ino].st = st_src;
+ }
+
+ // If destination existed and was overwritten, detach its mapping
+ if(dst_exists) {
+ auto it_dst = path_map.find(new_path);
+ if(it_dst != path_map.end()) {
+ fuse_ino_t dst_ino = it_dst->second;
+ path_map.erase(it_dst);
+
+ // Mark the old dst inode as disconnected (no pathname).
+ // Keep it alive until lookup_count==0 and open_count==0, then free
+ // in forget().
+ auto& dst_rec = ino_map[dst_ino];
+ dst_rec.path.clear();
+ }
+ }
+
+ // Refresh src inode attributes under its new name
+ struct stat st_new{};
+ if(gkfs::syscall::gkfs_stat(new_path, &st_new) == 0) {
+ ino_map[src_ino].st = st_new;
+ }
+ fuse_reply_err(req, 0);
+#else
+ fuse_reply_err(req, ENOTSUP);
+#endif
+}
+
+void
+mknod_handler(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode,
+ dev_t rdev) {
+ auto* ud = udata(req);
+ if(!ud->fifo) {
+ fuse_reply_err(req, ENOTSUP);
+ return;
+ }
+ auto* parent_inode = get_inode(parent);
+ if(!parent_inode) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
+
+ mkdir(fifo_path.c_str(), 0700);
+ std::string path = fifo_path + "/" + name;
+
+ if(!S_ISFIFO(mode)) {
+ fuse_reply_err(req, EINVAL);
+ return;
+ }
+
+ if(mkfifo(path.c_str(), mode) == -1) {
+ fuse_reply_err(req, errno);
+ return;
+ }
+
+ struct stat st{};
+ if(stat(path.c_str(), &st) == -1) {
+ fuse_reply_err(req, errno);
+ return;
+ }
+
+ // save synthetic entry
+ std::string vpath = get_path(parent_inode, name); // path in mount namespace
+ local_fifos[vpath] = st;
+
+ fuse_entry_param e{};
+ e.ino = st.st_ino;
+ e.attr = st;
+ e.attr_timeout = ud->timeout;
+ e.entry_timeout = ud->timeout;
+
+ fuse_reply_entry(req, &e);
+}
+
+static void
+init_gekkofs() {
+ // TODO how to handle mount point
+ int res = gkfs_init();
+ if(res != 0) {
+ printf("FUSE client failed to connect to gkfs daemon. Exit.");
+ exit(1);
+ }
+
+ auto fl = gkfs::syscall::gkfs_get_file_list("/");
+
+ for(std::string s : fl) {
+ std::cout << s << std::endl;
+ }
+
+ std::string root_path = "/";
+ struct stat st;
+ int rc = gkfs::syscall::gkfs_stat(root_path, &st);
+ if(rc < 0) {
+ fuse_log(FUSE_LOG_ERR, "failed to open root\n");
+ exit(1);
+ }
+ ino_map[FUSE_ROOT_ID] = {root_path, {}, 1};
+ ino_map[FUSE_ROOT_ID].st = st;
+ path_map[root_path] = FUSE_ROOT_ID;
+ std::cout << "root node allocated" << std::endl;
+}
+
+static void
+init_ll_ops(fuse_lowlevel_ops* ops) {
+ // file
+ ops->getattr = getattr_handler;
+ ops->setattr = setattr_handler;
+ ops->open = open_handler;
+ ops->create = create_handler;
+ ops->unlink = unlink_handler;
+ ops->forget = forget_handler;
+ // ops->forget_multi
+ ops->readlink = readlink_handler;
+ ops->mknod = mknod_handler;
+ ops->symlink = symlink_handler;
+ ops->rename = rename_handler;
+ // ops->link
+ ops->flush = flush_handler;
+ ops->release = release_handler;
+ ops->fsync = fsync_handler;
+ // ops->write_buf
+ ops->lseek = lseek_handler;
+
+ // xattr for arbitrary key value fields
+ // ops->setxattr
+ // ops->getxattr
+ // ops->listxattr
+ // ops->removexattr
+
+ // directory
+ ops->lookup = lookup_handler;
+ ops->mkdir = mkdir_handler;
+ ops->rmdir = rmdir_handler;
+ ops->readdir = readdir_handler;
+ ops->opendir = opendir_handler;
+ ops->releasedir = releasedir_handler;
+ // ops->fsyncdir = nullptr;
+ // ops->readdirplus
+
+ // I/O
+ ops->write = write_handler;
+ ops->read = read_handler;
+
+ // permission
+ ops->access = access_handler;
+
+ // misc
+ ops->init = init_handler;
+ ops->destroy = destroy_handler;
+ // ops->tmpfile // not supported in fuse < 3
+
+ // ops->statfs = nullptr;
+ // ops->flock
+ // ops->getlk
+ // ops->setlk
+ // ops->bmap
+ // ops->ioctl
+ // ops->poll
+ // ops->retrive_reply
+ // ops->fallocate
+ // ops->copy_file_range
+ // ops->statx // not supported in fuse < 3
+}
+
+void
+err_cleanup1(fuse_cmdline_opts opts, fuse_args& args, struct u_data& ud) {
+ free(opts.mountpoint);
+ free(ud.mountpoint);
+ fuse_opt_free_args(&args);
+ std::cout << "# Resources released" << std::endl;
+}
+
+void
+err_cleanup2(fuse_session& se) {
+ fuse_session_destroy(&se);
+ std::cout << "# Fuse session destroyed" << std::endl;
+}
+
+void
+err_cleanup3(fuse_session& se) {
+ fuse_remove_signal_handlers(&se);
+ std::cout << "# Signal handlers removed" << std::endl;
+}
+
+int
+main(int argc, char* argv[]) {
+ init_ll_ops(&ll_ops);
+
+ struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+ struct fuse_session* se;
+ struct fuse_cmdline_opts opts;
+ struct fuse_loop_config* config;
+ struct u_data ud{};
+ ud.debug = 0;
+ ud.writeback = 0;
+ int ret = -1;
+
+ /* Don't mask creation mode, kernel already did that */
+ umask(0); // TODO do we need this and why?
+
+ pthread_mutex_init(&ud.mutex, NULL);
+ ud.cache = CACHE_NORMAL;
+
+ if(fuse_parse_cmdline(&args, &opts) != 0)
+ return 1;
+ if(opts.show_help) {
+ printf("usage: %s [options] \n\n", argv[0]);
+ fuse_cmdline_help();
+ fuse_lowlevel_help();
+ passthrough_ll_help();
+ err_cleanup1(opts, args, ud);
+ return 0;
+ } else if(opts.show_version) {
+ printf("FUSE library version %s\n", fuse_pkgversion());
+ fuse_lowlevel_version();
+ ret = 0;
+ err_cleanup1(opts, args, ud);
+ return 0;
+ }
+
+ if(opts.mountpoint == NULL) {
+ printf("usage: %s [options] \n", argv[0]);
+ printf(" %s --help\n", argv[0]);
+ ret = 1;
+ err_cleanup1(opts, args, ud);
+ return 0;
+ }
+
+ if(fuse_opt_parse(&args, &ud, lo_opts, NULL) == -1)
+ return 1;
+
+ ud.debug = opts.debug;
+ if(!ud.timeout_set) {
+ switch(ud.cache) {
+ case CACHE_NEVER:
+ ud.timeout = 0.0;
+ break;
+
+ case CACHE_NORMAL:
+ ud.timeout = 1.0;
+ break;
+
+ case CACHE_ALWAYS:
+ ud.timeout = 86400.0;
+ break;
+ }
+ } else if(ud.timeout < 0) {
+ fuse_log(FUSE_LOG_ERR, "timeout is negative (%lf)\n", ud.timeout);
+ exit(1);
+ }
+
+
+ ud.mountpoint = strdup(opts.mountpoint);
+ init_gekkofs();
+
+
+ se = fuse_session_new(&args, &ll_ops, sizeof(ll_ops), &ud);
+ if(se == nullptr) {
+ err_cleanup1(opts, args, ud);
+ return 0;
+ }
+
+ if(fuse_set_signal_handlers(se) != 0) {
+ err_cleanup2(*se);
+ err_cleanup1(opts, args, ud);
+ return 0;
+ }
+
+ if(fuse_session_mount(se, opts.mountpoint) != 0) {
+ err_cleanup3(*se);
+ err_cleanup2(*se);
+ err_cleanup1(opts, args, ud);
+ return 0;
+ }
+
+ fuse_daemonize(opts.foreground);
+
+ /* Block until ctrl+c or fusermount -u */
+ if(opts.singlethread)
+ ret = fuse_session_loop(se);
+ else {
+#if FUSE_MAJOR_VERSION > 3 || \
+ (FUSE_MAJOR_VERSION == 3 && FUSE_MINOR_VERSION >= 12)
+ config = fuse_loop_cfg_create();
+ fuse_loop_cfg_set_clone_fd(config, opts.clone_fd);
+ fuse_loop_cfg_set_max_threads(config, opts.max_threads);
+ ret = fuse_session_loop_mt(se, config);
+ fuse_loop_cfg_destroy(config);
+ config = NULL;
+#else
+ // for older fuse versions like on the ubuntu22
+ ret = fuse_session_loop_mt(
+ se, NULL); // second argument should be checked!!!
+#endif
+ }
+
+ fuse_session_unmount(se);
+ return ret < 0 ? 1 : 0;
+}
diff --git a/src/client/gkfs_functions.cpp b/src/client/gkfs_functions.cpp
index 95405e0cc1cd2b1134f2bc6730bf80f12355df14..b12bfe073495b37295df6d0fed6059ce4e249477 100644
--- a/src/client/gkfs_functions.cpp
+++ b/src/client/gkfs_functions.cpp
@@ -185,7 +185,7 @@ test_lock_file(const std::string& path) {
* @param path
* @param mode
* @param flags
- * @return 0 on success, -1 on failure
+ * @return fd on success, -1 on failure
*/
int
gkfs_open(const std::string& path, mode_t mode, int flags) {
@@ -809,7 +809,7 @@ gkfs_statvfs(struct statvfs* buf) {
* @param fd
* @param offset
* @param whence
- * @return 0 on success, -1 on failure
+ * @return position on success, -1 on failure
*/
off_t
gkfs_lseek(unsigned int fd, off_t offset, unsigned int whence) {
@@ -822,7 +822,7 @@ gkfs_lseek(unsigned int fd, off_t offset, unsigned int whence) {
* @param gkfs_fd
* @param offset
* @param whence
- * @return 0 on success, -1 on failure
+ * @return position on success, -1 on failure
*/
off_t
gkfs_lseek(shared_ptr gkfs_fd, off_t offset,
@@ -1451,7 +1451,7 @@ gkfs_readv(int fd, const struct iovec* iov, int iovcnt) {
* wrapper function for opening directories
* errno may be set
* @param path
- * @return 0 on success or -1 on error
+ * @return fd on success or -1 on error
*/
int
gkfs_opendir(const std::string& path) {
@@ -1835,6 +1835,10 @@ gkfs_mk_symlink(const std::string& path, const std::string& target_path) {
errno = ENOTSUP;
return -1;
}
+ } else {
+ // target is not in gekkofs
+ errno = ENOENT;
+ return -1;
}
if(check_parent_dir(path)) {
@@ -1871,7 +1875,7 @@ gkfs_mk_symlink(const std::string& path, const std::string& target_path) {
* @param path
* @param buf
* @param bufsize
- * @return 0 on success or -1 on error
+ * @return path size on success or -1 on error
*/
int
gkfs_readlink(const std::string& path, char* buf, int bufsize) {
diff --git a/src/client/rpc/forward_metadata.cpp b/src/client/rpc/forward_metadata.cpp
index ad0882cd2b2842ccc496ba1b47ae1add0871e87d..d22842d1404a27307ae04c4c6c12f1325131f6a6 100644
--- a/src/client/rpc/forward_metadata.cpp
+++ b/src/client/rpc/forward_metadata.cpp
@@ -664,7 +664,7 @@ forward_get_metadentry_size(const std::string& path, const int copy) {
/**
* Send an RPC request to receive all entries of a directory.
* @param open_dir
- * @return error code
+ * @return error code, OpenDir
*/
pair>
forward_get_dirents(const string& path) {
@@ -780,9 +780,6 @@ forward_get_dirents(const string& path) {
bool* bool_ptr = reinterpret_cast(base_ptr);
char* names_ptr = reinterpret_cast(base_ptr) +
(out.dirents_size() * sizeof(bool));
- // Add special files like an standard fs.
- open_dir->add(".", gkfs::filemap::FileType::directory);
- open_dir->add("..", gkfs::filemap::FileType::directory);
for(std::size_t j = 0; j < out.dirents_size(); j++) {
gkfs::filemap::FileType ftype =
@@ -804,6 +801,9 @@ forward_get_dirents(const string& path) {
open_dir->add(name, ftype);
}
}
+ // Add special files like an standard fs.
+ open_dir->add(".", gkfs::filemap::FileType::directory);
+ open_dir->add("..", gkfs::filemap::FileType::directory);
return make_pair(err, open_dir);
}
diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt
index e7d7cccc141717f9a07031e338eeeffd4b53c81f..40204645583d82847b51f48677f274a210196a87 100644
--- a/tests/integration/CMakeLists.txt
+++ b/tests/integration/CMakeLists.txt
@@ -143,6 +143,15 @@ if (GKFS_RENAME_SUPPORT)
)
endif ()
+if (GKFS_BUILD_FUSE)
+ gkfs_add_python_test(
+ NAME test_fuse_client
+ PYTHON_VERSION 3.6
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests/integration
+ SOURCE fuse/
+ )
+endif ()
+
if (GKFS_INSTALL_TESTS)
install(DIRECTORY harness
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gkfs/tests/integration
@@ -239,6 +248,16 @@ if (GKFS_INSTALL_TESTS)
PATTERN ".pytest_cache" EXCLUDE
)
endif ()
+
+ if (GKFS_BUILD_FUSE)
+ install(DIRECTORY fuse
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gkfs/tests/integration
+ FILES_MATCHING
+ REGEX ".*\\.py"
+ PATTERN "__pycache__" EXCLUDE
+ PATTERN ".pytest_cache" EXCLUDE
+ )
+ endif ()
endif ()
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 9b3f318e6da70b0c4f9412614008d1f27b05d0fc..f0971f6905422985a144232da79e2652c22128d0 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -34,7 +34,7 @@ from pathlib import Path
from harness.logger import logger, initialize_logging, finalize_logging
from harness.cli import add_cli_options, set_default_log_formatter
from harness.workspace import Workspace, FileCreator
-from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
+from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator, FuseClient
from harness.reporter import report_test_status, report_test_headline, report_assertion_pass
def pytest_configure(config):
@@ -78,7 +78,7 @@ def caplog(test_workspace, request, _caplog):
def pytest_runtest_logreport(report):
"""
- Pytest hook called after a test phase (setup, call, teardownd)
+ Pytest hook called after a test phase (setup, call, teardownd)
has completed.
"""
@@ -123,7 +123,7 @@ def gkfs_daemon(request):
return request.getfixturevalue(request.param)
@pytest.fixture
-def gkfs_daemon_proxy(test_workspace, request):
+def gkfs_daemon_proxy(test_workspace, request):
interface = request.config.getoption('--interface')
daemon = Daemon(interface, "rocksdb", test_workspace, True)
@@ -177,7 +177,7 @@ def gkfs_shell(test_workspace):
"""
return ShellClient(test_workspace)
-
+
@pytest.fixture
def gkfs_shell_proxy(test_workspace):
"""
@@ -225,3 +225,15 @@ def gkfwd_client_factory(test_workspace):
"""
return FwdClientCreator(test_workspace)
+
+@pytest.fixture
+def fuse_client(test_workspace):
+ """
+ Sets up a gekkofs fuse client environment so that
+ operations (system calls, library calls, ...) can
+ be requested from a co-running daemon.
+ """
+
+ fuse_client = FuseClient(test_workspace)
+ yield fuse_client.run()
+ fuse_client.shutdown()
diff --git a/tests/integration/conftest.template b/tests/integration/conftest.template
index 9b3f318e6da70b0c4f9412614008d1f27b05d0fc..f0971f6905422985a144232da79e2652c22128d0 100644
--- a/tests/integration/conftest.template
+++ b/tests/integration/conftest.template
@@ -34,7 +34,7 @@ from pathlib import Path
from harness.logger import logger, initialize_logging, finalize_logging
from harness.cli import add_cli_options, set_default_log_formatter
from harness.workspace import Workspace, FileCreator
-from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator
+from harness.gkfs import Daemon, Client, ClientLibc, Proxy, ShellClient, ShellClientLibc, FwdDaemon, FwdClient, ShellFwdClient, FwdDaemonCreator, FwdClientCreator, FuseClient
from harness.reporter import report_test_status, report_test_headline, report_assertion_pass
def pytest_configure(config):
@@ -78,7 +78,7 @@ def caplog(test_workspace, request, _caplog):
def pytest_runtest_logreport(report):
"""
- Pytest hook called after a test phase (setup, call, teardownd)
+ Pytest hook called after a test phase (setup, call, teardownd)
has completed.
"""
@@ -123,7 +123,7 @@ def gkfs_daemon(request):
return request.getfixturevalue(request.param)
@pytest.fixture
-def gkfs_daemon_proxy(test_workspace, request):
+def gkfs_daemon_proxy(test_workspace, request):
interface = request.config.getoption('--interface')
daemon = Daemon(interface, "rocksdb", test_workspace, True)
@@ -177,7 +177,7 @@ def gkfs_shell(test_workspace):
"""
return ShellClient(test_workspace)
-
+
@pytest.fixture
def gkfs_shell_proxy(test_workspace):
"""
@@ -225,3 +225,15 @@ def gkfwd_client_factory(test_workspace):
"""
return FwdClientCreator(test_workspace)
+
+@pytest.fixture
+def fuse_client(test_workspace):
+ """
+ Sets up a gekkofs fuse client environment so that
+ operations (system calls, library calls, ...) can
+ be requested from a co-running daemon.
+ """
+
+ fuse_client = FuseClient(test_workspace)
+ yield fuse_client.run()
+ fuse_client.shutdown()
diff --git a/tests/integration/fuse/test_basic_operations.py b/tests/integration/fuse/test_basic_operations.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c0142a8456f742a5348c02afd4e6cad90ba97cf
--- /dev/null
+++ b/tests/integration/fuse/test_basic_operations.py
@@ -0,0 +1,92 @@
+################################################################################
+# Copyright 2018-2025, Barcelona Supercomputing Center (BSC), Spain #
+# Copyright 2015-2025, Johannes Gutenberg Universitaet Mainz, Germany #
+# #
+# This software was partially supported by the #
+# EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). #
+# #
+# This software was partially supported by the #
+# ADA-FS project under the SPPEXA project funded by the DFG. #
+# #
+# This file is part of GekkoFS. #
+# #
+# GekkoFS 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 #
+# (at your option) any later version. #
+# #
+# GekkoFS 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 GekkoFS. If not, see . #
+# #
+# SPDX-License-Identifier: GPL-3.0-or-later #
+################################################################################
+
+import harness
+from pathlib import Path
+import errno
+import stat
+import os
+import ctypes
+import sh
+import sys
+import pytest
+import time
+from harness.logger import logger
+
+nonexisting = "nonexisting"
+
+# somehow are multiple test causing an error in the fuse client...
+def test_read(gkfs_daemon, fuse_client):
+
+ file = gkfs_daemon.mountdir / "file"
+ file2 = gkfs_daemon.mountdir / "file2"
+
+ dir = gkfs_daemon.mountdir / "dir"
+
+ # creation and removal
+ sh.bash("-c", "echo baum > " + str(file))
+ assert sh.ls(fuse_client.mountdir) == "file\n"
+ assert sh.cat(file) == "baum\n"
+ sh.touch(str(file2))
+ assert sh.wc("-c", str(file2)) == "0 " + str(file2) + "\n"
+ sh.truncate("-s", "20", str(file2))
+ assert sh.wc("-c", str(file2)) == "20 " + str(file2) + "\n"
+ sh.mkdir(str(dir))
+ assert sh.ls(fuse_client.mountdir) == "dir file file2\n"
+ sh.cd(str(dir))
+ assert sh.pwd() == str(dir) + "\n"
+ sh.mkdir("-p", "foo/bar")
+ assert sh.ls() == "foo\n"
+ sh.cd("foo")
+ sh.rmdir("bar")
+ sh.cd("..")
+ sh.rmdir("foo")
+ sh.rm(str(file2))
+ assert sh.ls(fuse_client.mountdir) == "dir file\n"
+
+ # lseek test (TODO doesn't use the lseek handler)
+ path = gkfs_daemon.mountdir / "lseek_file"
+ with open(path, "wb") as f:
+ f.write(b"HelloWorld") # 10 bytes
+ fd = os.open(path, os.O_RDONLY)
+ pos = os.lseek(fd, 5, os.SEEK_SET) # absolute seek
+ assert pos == 5
+ data = os.read(fd, 5)
+ assert data == b"World"
+ os.close(fd)
+ os.remove(path)
+
+ # symlink
+ assert sh.pwd() == str(dir) + "\n"
+ sh.ln("-s", str(file), "link")
+ assert sh.ls(str(dir)) == "link\n"
+ assert sh.cat("link") == "baum\n"
+
+ # renaming
+ sh.mv("link", "../lonk")
+ assert sh.ls("..") == "dir file lonk\n"
diff --git a/tests/integration/harness/gkfs.py b/tests/integration/harness/gkfs.py
index e9722b5edfdf385f5cda2b74d6f6bee17287f743..6b906c5870268342a037c2b7245cd963963e615a 100644
--- a/tests/integration/harness/gkfs.py
+++ b/tests/integration/harness/gkfs.py
@@ -74,6 +74,7 @@ gkfwd_client_log_level = 'all'
gkfwd_client_log_syscall_filter = 'epoll_wait,epoll_create'
gkfwd_daemon_active_log_pattern = r'Startup successful. Daemon is ready.'
+gkfs_fuse_client = 'fuse_client'
def get_ip_addr(iface):
return netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr']
@@ -345,7 +346,7 @@ class Daemon:
logger.debug(f"daemon log file missing, checking if daemon is alive...")
pid=self._proc.pid
-
+
if not _process_exists(pid):
raise RuntimeError(f"process {pid} is not running")
@@ -392,7 +393,7 @@ class Proxy:
self._workspace = workspace
self._cmd = sh.Command(gkfs_proxy_cmd, self._workspace.bindirs)
self._env = os.environ.copy()
-
+
libdirs = ':'.join(
filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] +
[str(p) for p in self._workspace.libdirs]))
@@ -403,14 +404,14 @@ class Proxy:
'GKFS_PROXY_LOG_LEVEL': gkfs_proxy_log_level
}
self._env.update(self._patched_env)
-
+
def run(self):
args = ['-H', str(self.cwd / gkfs_hosts_file),
'--pid-path', str(self.cwd / gkfs_proxy_pid),
'-p', 'ofi+sockets']
-
+
logger.debug(f"spawning proxy")
logger.debug(f"cmdline: {self._cmd} " + " ".join(map(str, args)))
@@ -463,7 +464,7 @@ class Proxy:
The maximum number of log lines to check for a match.
"""
-
+
init_time = perf_counter()
@@ -479,7 +480,7 @@ class Proxy:
logger.debug(f"proxy log file missing, checking if daemon is alive...")
pid=self._proc.pid
-
+
if not _process_exists(pid):
raise RuntimeError(f"process {pid} is not running")
@@ -514,7 +515,7 @@ class Proxy:
@property
def interface(self):
return self._interface
-
+
class _proxy_exec():
def __init__(self, client, name):
@@ -609,7 +610,7 @@ class Client:
@property
def cwd(self):
return self._workspace.twd
-
+
class ClientLibc:
"""
A class to represent a GekkoFS client process with a patched LD_PRELOAD.
@@ -901,7 +902,7 @@ class ShellClient:
extra properties to it.
"""
-
+
found_cmd = shutil.which(cmd,
path=':'.join(str(p) for p in self._search_paths)
@@ -945,7 +946,7 @@ class ShellClient:
@property
def cwd(self):
return self._workspace.twd
-
+
class ShellClientLibc:
"""
@@ -1128,7 +1129,7 @@ class ShellClientLibc:
extra properties to it.
"""
-
+
found_cmd = shutil.which(cmd,
path=':'.join(str(p) for p in self._search_paths)
@@ -1637,3 +1638,82 @@ class ShellFwdClient:
@property
def cwd(self):
return self._workspace.twd
+
+class FuseClient:
+ def __init__(self, workspace):
+ self._workspace = workspace
+ #self._cmd = sh.Command("printenv", ["/usr/bin/"])#self._workspace.bindirs)
+ self._cmd = sh.Command(gkfs_fuse_client, self._workspace.bindirs)
+ self._env = os.environ.copy()
+ self._metadir = self.rootdir
+
+ libdirs = ':'.join(
+ filter(None, [os.environ.get('LD_LIBRARY_PATH', '')] +
+ [str(p) for p in self._workspace.libdirs]))
+
+ self._patched_env = {
+ 'LD_LIBRARY_PATH' : libdirs,
+ 'GKFS_HOSTS_FILE' : str(self.cwd / gkfs_hosts_file),
+ 'LIBGKFS_HOSTS_FILE' : str(self.cwd / gkfs_hosts_file), # TODO wtf why? see gkfs::env::HOSTS_FILE
+ }
+ self._env.update(self._patched_env)
+
+ def run(self):
+
+ args = [ "-f", "-s", self._workspace.mountdir, "-o", "auto_unmount" ]
+
+ print(f"spawning fuse client")
+ print(f"cmdline: {self._cmd} " + " ".join(map(str, args)))
+ print(f"patched env:\n{pformat(self._patched_env)}")
+
+ self._proc = self._cmd(
+ args,
+ _env=self._env,
+ _out='/dev/null',
+ _err='/dev/null',
+ _bg=True,
+ _ok_code=list(range(0, 256))
+ )
+
+ print(f"fuse client process spawned (PID={self._proc.pid})")
+ time.sleep(2) # give fuse time to register mount
+
+ return self
+
+ def shutdown(self):
+ try:
+ self._proc.terminate()
+ time.sleep(1) # give fuse time to unmount
+ err = self._proc.wait()
+ except ProcessLookupError:
+ print("Fuse client already gone at shutdown")
+ pass
+ except sh.SignalException_SIGTERM:
+ pass
+ except Exception:
+ pass
+
+
+ @property
+ def cwd(self):
+ return self._workspace.twd
+
+ @property
+ def rootdir(self):
+ return self._workspace.rootdir
+
+ @property
+ def mountdir(self):
+ return self._workspace.mountdir
+
+ @property
+ def bindirs(self):
+ return self._workspace.bindirs
+
+ @property
+ def logdir(self):
+ return self._workspace.logdir
+
+ @property
+ def env(self):
+ return self._env