// SPDX-License-Identifier: MIT
#if __linux__ || __APPLE__
// https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=981012#10 :)
// macFUSE yells at you to set this if you don't set this lol
#define _FILE_OFFSET_BITS 64
#elif __NetBSD__
// libpuffs uses vmsize_t and register_t from here but doesn't define the right macros to get them;
// if we do, we fuck up because they also control renames this is a high-quality hack, but it's valid WRT how these are actually defined
#include <machine/types.h>
typedef unsigned long vsize_t;
typedef __register_t register_t;
#endif
#define FUSE_USE_VERSION 30
#include <fuse.h>
#if __has_include(<fuse_lowlevel.h>)
#include <fuse_lowlevel.h>
#endif
#if FUSE_VERSION < 30
#define FUSE3_ONLY(...)
#else
#define FUSE3_ONLY(...) __VA_ARGS__
#endif
#include <assert.h>
#include <errno.h>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <atomic>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <vector>
#if __has_include(<charconv>)
#include <charconv>
static int parse_pid(const char * begin, const char * end, pid_t & into) {
auto [_, err] = std::from_chars(begin, end, into);
if(err != std::errc{})
return -(int)err;
else
return 0;
}
#else
#include <limits>
static int parse_pid(const char * begin, const char * end, pid_t & into) {
char buf[10 + 1]{}; // Enough for 2^31-1, nobody's PIDs are longer
std::strncpy(buf, begin, end - begin);
errno = 0;
char * err;
auto ret = std::strtoul(buf, &err, 10);
if(err == buf || *err != '\0')
return -EINVAL;
if(errno != 0)
return -errno;
if(ret > std::numeric_limits<pid_t>::max())
return -ERANGE;
into = ret;
return 0;
}
#endif
#include <thread>
#if __has_include(<pthread_np.h>)
#include <pthread_np.h>
#endif
#ifndef MSG_NOSIGNAL // just __APPLE__ at this point
#define MSG_NOSIGNAL 0
#define MSG_NOSIGNAL_MISSING 1
#endif
#include <febug-abi.h>
static timespec get_now() {
timespec ret;
clock_gettime(CLOCK_REALTIME, &ret);
return ret;
}
static const auto startup = get_now();
struct entry_t {
timespec added;
std::uint64_t type;
std::uint8_t signal;
std::string name;
};
struct process_t {
timespec connected;
uid_t owner_uid;
gid_t owner_gid;
int comm_fd = -1;
std::map<std::uint64_t, entry_t> entries;
};
static std::map<pid_t, process_t> processes{};
static std::shared_mutex processes_mtx{};
static bool debug = false;
template <class Root, class Process, class Entry>
static int dispatch_path(const std::string_view path, Root && root_f, Process && process_f, Entry && entry_f) {
if(path.size() == 0 || path.size() == 1) {
return root_f();
} else {
auto first_chunk = path.substr(1);
const auto first_chunk_end_idx = first_chunk.find('/');
first_chunk = first_chunk.substr(0, first_chunk_end_idx);
pid_t val{};
if(auto err = parse_pid(first_chunk.begin(), first_chunk.end(), val)) {
if(debug)
std::cerr << path << ": " << first_chunk << ": " << std::strerror(err) << '\n';
return -ENOENT;
}
const std::shared_lock p_lock{processes_mtx};
if(const auto process = processes.find(val); process != processes.end()) {
if(first_chunk_end_idx == std::string_view::npos) {
return process_f(process);
} else {
auto name_chunk = path.substr(1 + first_chunk_end_idx + 1);
if(const auto entry =
std::find_if(process->second.entries.begin(), process->second.entries.end(), [&](auto && ent) { return ent.second.name == name_chunk; });
entry != process->second.entries.end()) {
return entry_f(process, entry);
} else {
if(debug)
std::cerr << path << ": " << name_chunk << '\n';
return -ENOENT;
}
}
} else
return -ENOENT;
}
}
#if __APPLE__
#define st_mtim st_mtimespec
#define st_ctim st_ctimespec
#endif
static void root_stat(struct stat & sb) {
sb.st_mode = S_IFDIR | 0555;
sb.st_nlink = 2 + processes.size();
sb.st_mtim = sb.st_ctim = startup;
}
static void process_stat(struct stat & sb, const process_t & process) {
sb.st_mtim = sb.st_ctim = process.connected;
sb.st_uid = process.owner_uid;
sb.st_gid = process.owner_gid;
sb.st_mode = S_IFDIR | 0550;
sb.st_nlink = 2;
}
static void entry_stat(struct stat & sb, const process_t & process, const entry_t & entry) {
sb.st_mtim = sb.st_ctim = entry.added;
sb.st_uid = process.owner_uid;
sb.st_gid = process.owner_gid;
sb.st_mode = S_IFREG | 0440;
sb.st_nlink = 1;
}
struct read_state {
off_t offset{};
int read_end = -1;
};
static const fuse_operations fops = [] {
fuse_operations ops{};
/** Get file attributes.
*
* Similar to stat(). The 'st_dev' and 'st_blksize' fields are
* ignored. The 'st_ino' field is ignored except if the 'use_ino'
* mount option is given. In that case it is passed to userspace,
* but libfuse and the kernel will still assign a different
* inode for internal use (called the "nodeid").
*
* `fi` will always be NULL if the file is not currently open, but
* may also be NULL if the file is open.
*/
ops.getattr = [](const char * path, struct stat * ret FUSE3_ONLY(, struct fuse_file_info *)) -> int {
return dispatch_path(
path,
[&] {
root_stat(*ret);
return 0;
},
[&](auto && process) {
process_stat(*ret, process->second);
return 0;
},
[&](auto && process, auto && entry) {
entry_stat(*ret, process->second, entry->second);
return 0;
});
};
/** Open a file
*
* Open flags are available in fi->flags. The following rules
* apply.
*
* - Creation (O_CREAT, O_EXCL, O_NOCTTY) flags will be
* filtered out / handled by the kernel.
*
* - Access modes (O_RDONLY, O_WRONLY, O_RDWR) should be used
* by the filesystem to check if the operation is
* permitted. If the ``-o default_permissions`` mount
* option is given, this check is already done by the
* kernel before calling open() and may thus be omitted by
* the filesystem.
*
* - When writeback caching is enabled, the kernel may send
* read requests even for files opened with O_WRONLY. The
* filesystem should be prepared to handle this.
*
* - When writeback caching is disabled, the filesystem is
* expected to properly handle the O_APPEND flag and ensure
* that each write is appending to the end of the file.
*
* - When writeback caching is enabled, the kernel will
* handle O_APPEND. However, unless all changes to the file
* come through the kernel this will not work reliably. The
* filesystem should thus either ignore the O_APPEND flag
* (and let the kernel handle it), or return an error
* (indicating that reliably O_APPEND is not available).
*
* Filesystem may store an arbitrary file handle (pointer,
* index, etc) in fi->fh, and use this in other all other file
* operations (read, write, flush, release, fsync).
*
* Filesystem may also implement stateless file I/O and not store
* anything in fi->fh.
*
* There are also some flags (direct_io, keep_cache) which the
* filesystem may set in fi, to change the way the file is opened.
* See fuse_file_info structure in <fuse_common.h> for more details.
*
* If this request is answered with an error code of ENOSYS
* and FUSE_CAP_NO_OPEN_SUPPORT is set in
* `fuse_conn_info.capable`, this is treated as success and
* future calls to open will also succeed without being send
* to the filesystem process.
*
*/
ops.open = [](const char * path, struct fuse_file_info * fi) -> int {
return dispatch_path(
path, [] { return 0; }, [](auto &&) { return 0; },
[&](auto && process, auto && entry) {
if(fi->flags & O_DIRECTORY)
return -EISDIR;
else if(fi->flags & (O_WRONLY | O_RDWR))
return -EROFS;
#if __linux__
else if(fi->flags & O_PATH)
return -EOPNOTSUPP;
#endif
int commie_pipe[2];
if(pipe(commie_pipe) == -1)
return -ENOMEM;
fi->direct_io = true;
fi->fh = (std::uint64_t) new read_state{0, commie_pipe[0]};
// https://stackoverflow.com/a/15313135/2851815
char cmsgbuf[CMSG_SPACE(sizeof(commie_pipe[1]))];
attn_febug_message afmsg{.variable_id = entry->first, .variable_type = entry->second.type};
iovec afmsg_i{&afmsg, sizeof(afmsg)};
msghdr msg{};
msg.msg_iov = &afmsg_i;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf); // necessary for CMSG_FIRSTHDR to return the correct value
auto cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(commie_pipe[1]));
memcpy(CMSG_DATA(cmsg), &commie_pipe[1], sizeof(commie_pipe[1]));
msg.msg_controllen = cmsg->cmsg_len; // total size of all control blocks
if((sendmsg(process->second.comm_fd, &msg, MSG_NOSIGNAL)) == -1) {
std::cerr << path << ": open: sendmsg: " << std::strerror(errno) << '\n';
return -EBUSY;
}
close(commie_pipe[1]);
if(entry->second.signal != SIGKILL)
if(kill(process->first, (int)entry->second.signal) == -1)
std::cerr << path << ": open: kill: " << std::strerror(errno) << '\n';
return 0;
});
};
/** Read data from an open file
*
* Read should return exactly the number of bytes requested except
* on EOF or error, otherwise the rest of the data will be
* substituted with zeroes. An exception to this is when the
* 'direct_io' mount option is specified, in which case the return
* value of the read system call will reflect the return value of
* this operation.
*/
ops.read = [](const char *, char * obuf, size_t obuflen, off_t offset, struct fuse_file_info * fi) -> int {
if(fi->fh) {
auto state = (read_state *)fi->fh;
if(offset != state->offset)
return -EBADF;
if(auto ret = read(state->read_end, obuf, obuflen); ret != -1) {
state->offset += ret;
return ret;
} else
return -errno;
} else
return -ENOENT;
};
/** Release an open file
*
* Release is called when there are no more references to an open
* file: all file descriptors are closed and all memory mappings
* are unmapped.
*
* For every open() call there will be exactly one release() call
* with the same flags and file descriptor. It is possible to
* have a file opened more than once, in which case only the last
* release will mean, that no more reads/writes will happen on the
* file. The return value of release is ignored.
*/
ops.release = [](const char *, struct fuse_file_info * fi) -> int {
if(fi->fh) {
auto state = (read_state *)fi->fh;
close(state->read_end);
delete state;
fi->fh = 0;
}
return 0;
};
/** Open directory
*
* Unless the 'default_permissions' mount option is given,
* this method should check if opendir is permitted for this
* directory. Optionally opendir may also return an arbitrary
* filehandle in the fuse_file_info structure, which will be
* passed to readdir, releasedir and fsyncdir.
*
* default_permissions is always set
*/
ops.opendir = [](const char * path, struct fuse_file_info *) -> int {
return dispatch_path(
path, [] { return 0; }, [](auto &&) { return 0; }, [](auto &&, auto &&) { return -ENOTDIR; });
};
/** Read directory
*
* The filesystem may choose between two modes of operation:
*
* 1) The readdir implementation ignores the offset parameter, and
* passes zero to the filler function's offset. The filler
* function will not return '1' (unless an error happens), so the
* whole directory is read in a single readdir operation.
*
* 2) The readdir implementation keeps track of the offsets of the
* directory entries. It uses the offset parameter and always
* passes non-zero offset to the filler function. When the buffer
* is full (or an error happens) the filler function will return
* '1'.
*/
ops.readdir = [](const char * path, void * dest, fuse_fill_dir_t op, off_t, struct fuse_file_info * FUSE3_ONLY(, fuse_readdir_flags)) -> int {
return dispatch_path(
path,
[&] {
char buf[20 + 1]{};
struct stat sb;
for(auto && [pid, process] : processes) {
snprintf(buf, sizeof(buf), "%d", pid);
process_stat(sb, process);
if(op(dest, buf, &sb, 0 FUSE3_ONLY(, FUSE_FILL_DIR_PLUS)) == 1)
return -EBUSY;
}
return 0;
},
[&](auto && process) {
struct stat sb;
for(auto && [id, entry] : process->second.entries) {
(void)id;
entry_stat(sb, process->second, entry);
if(op(dest, entry.name.c_str(), &sb, 0 FUSE3_ONLY(, FUSE_FILL_DIR_PLUS)) == 1)
return -EBUSY;
}
return 0;
},
[](auto &&, auto &&) {
__builtin_unreachable();
return 0;
});
};
return ops;
}();
#if __linux__
#define DEFAULT_SOCK "/run/febug.sock"
#else
#define DEFAULT_SOCK "/var/run/febug.sock"
#endif
#if __APPLE__
// GUESS WHAT?? DARWIN DEFINES SOCK_SEQPACKET BUT DOESN'T ACTUALLY FUCKING SUPPORT IT (i.e. socket(2) returns EPROTONOSUPPORT). WHY? BECAUSE FUCK YOU.
#undef SOCK_SEQPACKET
#define SOCK_SEQPACKET SOCK_STREAM
#endif
struct control_socket {
const char * path = nullptr;
int fd = -1;
int init() {
if((this->fd = socket(AF_UNIX, SOCK_SEQPACKET, 0)) == -1) {
std::cerr << "socket: " << std::strerror(errno) << '\n';
return 1;
}
const char * path = DEFAULT_SOCK;
if(auto pe = std::getenv("FEBUG_SOCKET"))
path = pe;
if(unlink(path) == -1 && errno != ENOENT) {
std::cerr << "unlink(" << path << "): " << std::strerror(errno) << '\n';
return 1;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
if(bind(this->fd, (const sockaddr *)&addr, sizeof(addr)) == -1) {
std::cerr << "bind(" << addr.sun_path << "): " << std::strerror(errno) << '\n';
return 1;
}
chmod(path, 0777);
if(listen(this->fd, 50) == -1) {
std::cerr << "listen: " << std::strerror(errno) << '\n';
unlink(path);
return 1;
}
if(!(this->path = realpath(addr.sun_path, nullptr))) {
std::cerr << "realpath(" << addr.sun_path << "): " << std::strerror(errno) << '\n';
return 1;
}
return 0;
}
~control_socket() {
close(this->fd);
if(this->path && unlink(this->path) == -1)
std::cerr << "unlink(" << this->path << "): " << std::strerror(errno) << '\n';
free((void *)this->path);
this->path = nullptr;
}
};
void process_handler(int cs, std::atomic<bool> * keep_going) {
std::vector<pollfd> listening_to{{cs, POLLIN, 0}}; // first is control socket, rest are sorted connections
std::map<int, pid_t> process_pids;
std::vector<int> to_remove;
#if __linux__
using cmsgcred = ucred;
#define SCM_CREDS SCM_CREDENTIALS
#define cmcred_pid pid
#define cmcred_uid uid
#define cmcred_gid gid
#define SOL_CREDS_LEVEL SOL_SOCKET
#elif __NetBSD__
// unix(4) recommends SOCKCREDSIZE(ngroups), but we use none, so we're fine with the default (1)
using cmsgcred = sockcred;
#define cmcred_pid sc_pid
#define cmcred_uid sc_uid
#define cmcred_gid sc_gid
#define SOL_CREDS_LEVEL 0
#define SO_PASSCRED LOCAL_CREDS
#elif __OpenBSD__
using cmsgcred = sockpeercred;
#define cmcred_pid pid
#define cmcred_uid uid
#define cmcred_gid gid
#elif __APPLE__
struct cmsgcred {
uid_t cmcred_uid;
gid_t cmcred_gid;
pid_t cmcred_pid;
cmsgcred & operator=(const xucred & ucred) {
this->cmcred_uid = ucred.cr_uid;
this->cmcred_gid = ucred.cr_gid;
return *this;
}
operator bool() { return true; } // cool hack, Ha Six!
};
#endif
const auto auth_process = [&](int fd, cmsgcred & process_creds) {
if(debug)
std::cerr << "fd " << fd << ": is PID " << process_creds.cmcred_pid << " and authed as " << process_creds.cmcred_uid << '/' << process_creds.cmcred_gid
<< '\n';
#if MSG_NOSIGNAL_MISSING
int dont = 1;
if(fcntl(fd, F_SETNOSIGPIPE, &dont) == -1)
std::cerr << "fd " << fd << " (" << process_creds.cmcred_pid << "): fcntl(F_SETNOSIGPIPE, 1): " << std::strerror(errno) << '\n';
#endif
auto ppid = process_pids.emplace(fd, process_creds.cmcred_pid).first;
{
const std::unique_lock p_lock{processes_mtx};
processes.emplace(process_creds.cmcred_pid, process_t{get_now(), process_creds.cmcred_uid, process_creds.cmcred_gid, fd, {}});
}
return ppid;
};
while(poll(listening_to.data(), listening_to.size(), 250) != -1) {
if(!*keep_going)
return;
for(auto itr = listening_to.begin() + 1; itr != listening_to.end(); ++itr) {
if(!itr->revents)
continue;
auto ppid = process_pids.find(itr->fd);
if(debug)
std::cerr << "fd " << itr->fd << '(' << (ppid == process_pids.end() ? -1 : ppid->second) << "): has " << itr->revents << '\n';
#if __NetBSD__ // If peer closes, we get POLLIN instead of POLLHUP; this is known as "good design"
if(itr->revents & POLLIN) {
send(itr->fd, nullptr, 0, MSG_NOSIGNAL | MSG_OOB);
if(errno == EOPNOTSUPP)
; // MSG_OOB doesn't work with SOCK_SEQPACKET; that's good!
else if(errno == EPIPE)
itr->revents |= POLLHUP;
else
std::cerr << "fd " << itr->fd << ": send(MSG_OOB): unexpected error " << std::strerror(errno) << '\n';
}
#endif
if(itr->revents & POLLHUP) {
to_remove.emplace_back(itr->fd);
continue;
}
if(itr->revents & POLLIN) {
char buf[4096];
ssize_t cnt = -1;
#if __OpenBSD__ || __APPLE__ // getsockopt(SO_PEERCRED) at accept()
assert(ppid != process_pids.end());
#else
if(ppid == process_pids.end()) {
iovec buf_i{buf, sizeof(buf)};
char cmsgbuf[CMSG_SPACE(sizeof(cmsgcred))];
msghdr msg{};
msg.msg_iov = &buf_i;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
cnt = recvmsg(itr->fd, &msg, 0);
auto e = errno;
cmsgcred process_creds{};
if(auto cmsg = CMSG_FIRSTHDR(&msg); cmsg != nullptr && cmsg->cmsg_type == SCM_CREDS) {
memcpy(&process_creds, CMSG_DATA(cmsg), sizeof(process_creds));
ppid = auth_process(itr->fd, process_creds);
} else {
std::cerr << "fd " << itr->fd << ": didn't send creds!\n";
continue;
}
#if __linux__ // NetBSD clears the flag after first message (see unp_send() in sys/kern/uipc_usrreq.c), no need to enter the kernel again
const int no = 0;
setsockopt(itr->fd, SOL_CREDS_LEVEL, SO_PASSCRED, &no, sizeof(no));
#endif
errno = e;
} else
#endif
cnt = recv(itr->fd, buf, sizeof(buf), 0);
if(cnt == -1)
std::cerr << "fd " << itr->fd << '(' << ppid->second << "): recv: " << std::strerror(errno) << '\n';
else if(cnt == sizeof(febug_message)) {
const febug_message * fmsg = (febug_message *)buf;
std::string_view name{fmsg->name, sizeof(fmsg->name)};
if(auto pos = name.find('\0'); pos != std::string_view::npos)
name = name.substr(0, pos);
if(debug)
std::cerr << "fd " << itr->fd << '(' << ppid->second << "): " << fmsg->variable_id << ", " << fmsg->variable_type << ", " << strsignal(fmsg->signal)
<< ", " << name << '\n';
{
const std::unique_lock p_lock{processes_mtx};
processes.find(ppid->second)
->second.entries.insert_or_assign(fmsg->variable_id, entry_t{get_now(), fmsg->variable_type, fmsg->signal, std::string{name}});
}
} else if(cnt == sizeof(stop_febug_message)) {
const stop_febug_message * sfmsg = (stop_febug_message *)buf;
if(debug)
std::cerr << "fd " << itr->fd << '(' << ppid->second << "): stop " << sfmsg->variable_id << '\n';
{
const std::unique_lock p_lock{processes_mtx};
processes.find(ppid->second)->second.entries.erase(sfmsg->variable_id);
}
} else if(cnt == 0)
; // just auth
else
std::cerr << "fd " << itr->fd << '(' << ppid->second << ": unknown length " << cnt << '\n';
}
}
for(auto fd : to_remove) {
// stolen from std::binary_search
if(auto itr = std::lower_bound(listening_to.begin() + 1, listening_to.end(), fd, [](const pollfd & lhs, int rhs) { return lhs.fd < rhs; });
itr != listening_to.end() && !(fd < itr->fd)) {
close(fd);
listening_to.erase(itr);
} else {
std::cerr << "fd " << fd << ": staged for removal, but doesn't exist?\n";
// Weird, shouldn't happen. but what can you do
}
if(auto itr = process_pids.find(fd); itr != process_pids.end()) {
{
const std::unique_lock p_lock{processes_mtx};
processes.erase(itr->second);
}
process_pids.erase(itr);
} else {
// Client disconnected without auth
}
}
to_remove.clear();
if(listening_to[0].revents) {
if(debug)
std::cerr << "cs: " << listening_to[0].revents << '\n';
if(int to_add = accept(cs, nullptr, nullptr); to_add != -1) {
#if __linux__ || __NetBSD__
// https://stackoverflow.com/a/15313135/2851815
// Ideally we'd just have the client explicitly send us these once;
// however, this did not work with SOCK_SEQPACKET on Linux 4.19.0-13-amd64.
// We do this on FreeBSD tho
// This is the recommended-by-unix(4) way to do this on NetBSD
const int yes = 1;
if(setsockopt(to_add, SOL_CREDS_LEVEL, SO_PASSCRED, &yes, sizeof(yes)) == -1)
std::cerr << "fd " << to_add << ": setsockopt(SO_PASSCRED): " << std::strerror(errno) << '\n';
#endif
#if __OpenBSD__ || __APPLE__
cmsgcred process_creds{};
#if __OpenBSD__
socklen_t process_creds_len = sizeof(process_creds);
if(getsockopt(to_add, SOL_SOCKET, SO_PEERCRED, &process_creds, &process_creds_len) != -1 && process_creds_len == sizeof(process_creds))
#else
socklen_t opt_len = sizeof(pid_t);
if(!(getsockopt(to_add, SOL_LOCAL, LOCAL_PEEREPID, &process_creds.cmcred_pid, &opt_len) != -1 && opt_len == sizeof(pid_t))) {
std::cerr << "fd " << to_add << ": getsockopt(LOCAL_PEEREPID): " << std::strerror(errno) << ", dropping" << '\n';
close(to_add);
continue;
}
xucred ucred{};
opt_len = sizeof(xucred);
if(getsockopt(to_add, SOL_LOCAL, LOCAL_PEERCRED, &ucred, &opt_len) != -1 && opt_len == sizeof(ucred) && (process_creds = ucred))
#endif
auth_process(to_add, process_creds);
else {
std::cerr << "fd " << to_add << ": getsockopt(SO_PEERCRED): " << std::strerror(errno) << ", dropping" << '\n';
close(to_add);
continue;
}
#endif
#if __NetBSD__
// LOCAL_CREDS only applies to messages sent after we set the flag, which clients might well outrace us to; send a sync message
const attn_febug_message syncm{};
send(to_add, &syncm, sizeof(syncm), MSG_NOSIGNAL);
#endif
listening_to.emplace(std::upper_bound(listening_to.begin() + 1, listening_to.end(), to_add, [](int lhs, const pollfd & rhs) { return lhs < rhs.fd; }),
pollfd{to_add, POLLIN, 0});
} else
std::cerr << "accept: " << std::strerror(errno) << '\n';
}
}
std::cerr << "poll: " << std::strerror(errno) << '\n';
}
enum fuse_tag {};
template <class = void>
fuse_tag fuse_package_version();
template <class = void>
fuse_tag fuse_cmdline_help();
fuse_tag fuse_lib_help(...);
int main(int argc, char ** argv) {
static_assert(std::atomic<bool>::is_always_lock_free);
std::vector<const char *> true_argv(argv, argv + argc);
if(std::any_of(argv + 1, argv + argc, [](std::string_view arg) { return arg == "-h" || arg == "--help"; })) {
std::cout << "usage: " << *argv
<< " [-h|--help] [-v|--version] [-d] [FUSE options] <mountpoint>\n"
"\n"
"FUSE options (-f"
<< ((geteuid() == 0) ? ", -o default_permissions, and -o allow_other" : " and -o default_permissions") << " are set by default):\n";
if constexpr(std::is_same_v<decltype(fuse_lib_help(nullptr)), fuse_tag>) {
if constexpr(std::is_same_v<decltype(fuse_cmdline_help()), fuse_tag>)
std::cout << "Stingy libfuse, none known!\n"
"Assuming at least:\n"
"\t-f run in foreground\n"
"\t-o default_permissions check file permissions in kernel\n"
"\t-o allow_other allow access by all users\n"
"\n";
else
fuse_cmdline_help();
} else {
fuse_args fa{argc, argv, false};
fuse_lib_help(&fa);
}
std::cout << "\n"
"Environment variables:\n"
"\tFEBUG_SOCKET=["
<< DEFAULT_SOCK << "]\n";
return 0;
}
if(std::any_of(argv + 1, argv + argc, [](std::string_view arg) { return arg == "-V" || arg == "--version"; })) {
std::cout << "febug " << FEBUG_VERSION
<< "\n"
"FUSE ";
if constexpr(std::is_same_v<decltype(fuse_package_version()), fuse_tag>) {
const auto ver = fuse_version();
std::cout << (ver / 10) << '.' << (ver % 10);
} else
std::cout << fuse_package_version();
std::cout << '\n';
return 0;
}
if(std::find(argv + 1, argv + argc, std::string_view{"-d"}) != argv + argc) {
true_argv.emplace_back("-d");
debug = true;
}
true_argv.emplace_back("-f"); // fuse_main() never returns if this isn't present
true_argv.emplace_back("-o");
true_argv.emplace_back("default_permissions");
if(geteuid() == 0) {
true_argv.emplace_back("-o");
true_argv.emplace_back("allow_other");
}
control_socket cs;
if(int ret = cs.init())
return ret;
std::atomic<bool> keep_going = true;
std::thread hp{process_handler, cs.fd, &keep_going};
#if __linux__
pthread_setname_np(hp.native_handle(), "process_handler");
#elif __NetBSD__
pthread_setname_np(hp.native_handle(), "process_handler", nullptr);
#elif !__APPLE__ // Darwin can only setname_np for current thread
pthread_set_name_np(hp.native_handle(), "process_handler");
#endif
auto ret = fuse_main(true_argv.size(), (char **)true_argv.data(), &fops, nullptr);
keep_going = false;
hp.join();
return ret;
}