/* Copyright (c) 2017-2026 Hans-Kristian Arntzen * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "fs-netfs.hpp" #include "path_utils.hpp" #include "logging.hpp" #include #include #define HOST_IP "localhost" namespace Granite { struct FSNotifyCommand : LooperHandler { FSNotifyCommand(const string &protocol, unique_ptr socket_) : LooperHandler(move(socket_)), expected(false) { reply_queue.emplace(); auto &reply = reply_queue.back(); reply.builder.add_u32(NETFS_NOTIFICATION); reply.builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST); reply.builder.add_string(protocol); reply.writer.start(reply.builder.get_buffer()); result_reply.begin(4 * sizeof(uint32_t)); command_reader.start(result_reply.get_buffer()); state = NotificationLoop; } void expected_destruction() { expected = true; } ~FSNotifyCommand() { if (!expected) terminate(); } void set_notify_cb(function func) { notify_cb = move(func); } void push_register_notification(const string &path, promise result) { if (reply_queue.empty() && socket->get_parent_looper()) socket->get_parent_looper()->modify_handler(EVENT_IN | EVENT_OUT, *this); reply_queue.emplace(); auto &reply = reply_queue.back(); reply.builder.add_u32(NETFS_REGISTER_NOTIFICATION); reply.builder.add_string(path); reply.writer.start(reply.builder.get_buffer()); replies.push(move(result)); } void push_unregister_notification(FileNotifyHandle handler, promise result) { if (reply_queue.empty() && socket->get_parent_looper()) socket->get_parent_looper()->modify_handler(EVENT_IN | EVENT_OUT, *this); reply_queue.emplace(); auto &reply = reply_queue.back(); reply.builder.add_u32(NETFS_UNREGISTER_NOTIFICATION); reply.builder.add_u64(8); reply.builder.add_u64(uint64_t(handler)); reply.writer.start(reply.builder.get_buffer()); replies.push(move(result)); } void modify_looper(Looper &looper) { uint32_t mask = reply_queue.empty() ? EVENT_IN : (EVENT_IN | EVENT_OUT); looper.modify_handler(mask, *this); } bool read_reply_data(Looper &looper) { auto ret = command_reader.process(*socket); if (command_reader.complete()) { if (last_cmd == NETFS_BEGIN_CHUNK_NOTIFICATION) { FileNotifyInfo info; info.path = result_reply.read_string(); info.handle = FileNotifyHandle(result_reply.read_u64()); auto type = result_reply.read_u32(); switch (type) { case NETFS_FILE_CHANGED: info.type = FileNotifyType::FileChanged; break; case NETFS_FILE_DELETED: info.type = FileNotifyType::FileDeleted; break; case NETFS_FILE_CREATED: info.type = FileNotifyType::FileCreated; break; } notify_cb(info); result_reply.begin(4 * sizeof(uint32_t)); command_reader.start(result_reply.get_buffer()); modify_looper(looper); state = NotificationLoop; return true; } else if (last_cmd == NETFS_BEGIN_CHUNK_REPLY) { auto handle = int(result_reply.read_u64()); try { replies.front().set_value(handle); } catch (...) { } assert(!replies.empty()); replies.pop(); result_reply.begin(4 * sizeof(uint32_t)); command_reader.start(result_reply.get_buffer()); modify_looper(looper); state = NotificationLoop; return true; } else return false; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } bool notification_loop(Looper &looper, EventFlags flags) { if (flags & EVENT_OUT) { if (reply_queue.empty()) { looper.modify_handler(EVENT_IN, *this); return true; } auto ret = reply_queue.front().writer.process(*socket); if (reply_queue.front().writer.complete()) reply_queue.pop(); if (reply_queue.empty()) { looper.modify_handler(EVENT_IN, *this); return true; } else return (ret > 0) || (ret == Socket::ErrorWouldBlock); } if (flags & EVENT_IN) { auto ret = command_reader.process(*socket); if (command_reader.complete()) { auto cmd = result_reply.read_u32(); if (cmd == NETFS_BEGIN_CHUNK_NOTIFICATION || cmd == NETFS_BEGIN_CHUNK_REPLY) { if (result_reply.read_u32() != NETFS_ERROR_OK) return false; last_cmd = cmd; auto size = result_reply.read_u64(); if (size) { // Either receive notification or acknowledgement. result_reply.begin(size); command_reader.start(result_reply.get_buffer()); state = ReadReplyData; looper.modify_handler(EVENT_IN, *this); } else { // Acknowledge unregister notification. try { replies.front().set_value(0); } catch (...) { } assert(!replies.empty()); replies.pop(); result_reply.begin(4 * sizeof(uint32_t)); command_reader.start(result_reply.get_buffer()); modify_looper(looper); state = NotificationLoop; } return true; } else return false; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } return true; } bool handle(Looper &looper, EventFlags flags) override { if (state == ReadReplyData) return read_reply_data(looper); else if (state == NotificationLoop) return notification_loop(looper, flags); else return false; } enum State { ReadReplyData, NotificationLoop }; State state = NotificationLoop; SocketReader command_reader; ReplyBuilder result_reply; uint32_t last_cmd = 0; struct NotificationReply { SocketWriter writer; ReplyBuilder builder; }; queue reply_queue; queue> replies; function notify_cb; atomic_bool expected; }; struct FSReadCommand : LooperHandler { virtual ~FSReadCommand() = default; FSReadCommand(const string &path, NetFSCommand command, unique_ptr socket_) : LooperHandler(move(socket_)) { reply_builder.begin(); reply_builder.add_u32(command); reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST); reply_builder.add_string(path); command_writer.start(reply_builder.get_buffer()); state = WriteCommand; } bool write_command(Looper &looper) { auto ret = command_writer.process(*socket); if (command_writer.complete()) { state = ReadReplySize; reply_builder.begin(4 * sizeof(uint32_t)); command_reader.start(reply_builder.get_buffer()); looper.modify_handler(EVENT_IN, *this); return true; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } bool read_reply_size(Looper &) { auto ret = command_reader.process(*socket); if (command_reader.complete()) { if (reply_builder.read_u32() != NETFS_BEGIN_CHUNK_REPLY) return false; if (reply_builder.read_u32() != NETFS_ERROR_OK) return false; uint64_t reply_size = reply_builder.read_u64(); if (reply_size == 0) return false; reply_builder.begin(reply_size); command_reader.start(reply_builder.get_buffer()); state = ReadReply; return true; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } bool read_reply(Looper &) { auto ret = command_reader.process(*socket); if (command_reader.complete()) { parse_reply(); return false; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } bool handle(Looper &looper, EventFlags) override { if (state == WriteCommand) return write_command(looper); else if (state == ReadReplySize) return read_reply_size(looper); else if (state == ReadReply) return read_reply(looper); else return false; } enum State { WriteCommand, ReadReplySize, ReadReply }; State state = WriteCommand; SocketReader command_reader; SocketWriter command_writer; ReplyBuilder reply_builder; virtual void parse_reply() = 0; }; struct FSReader : FSReadCommand { FSReader(const string &path, unique_ptr socket_) : FSReadCommand(path, NETFS_READ_FILE, move(socket_)) { } ~FSReader() { if (!got_reply) result.set_exception(make_exception_ptr(runtime_error("file read"))); } void parse_reply() override { got_reply = true; try { result.set_value(reply_builder.consume_buffer()); } catch (...) { } } promise> result; bool got_reply = false; }; struct FSList : FSReadCommand { FSList(const string &path, unique_ptr socket_) : FSReadCommand(path, NETFS_LIST, move(socket_)) { } ~FSList() { if (!got_reply) result.set_exception(make_exception_ptr(runtime_error("List failed"))); } void parse_reply() override { uint32_t entries = reply_builder.read_u32(); vector list; for (uint32_t i = 0; i < entries; i++) { auto path = reply_builder.read_string(); auto type = reply_builder.read_u32(); switch (type) { case NETFS_FILE_TYPE_PLAIN: list.push_back({ move(path), PathType::File }); break; case NETFS_FILE_TYPE_DIRECTORY: list.push_back({ move(path), PathType::Directory }); break; case NETFS_FILE_TYPE_SPECIAL: list.push_back({ move(path), PathType::Special }); break; } } got_reply = true; try { result.set_value(move(list)); } catch (...) { } } promise> result; bool got_reply = false; }; struct FSStat : FSReadCommand { FSStat(const string &path, unique_ptr socket_) : FSReadCommand(path, NETFS_STAT, move(socket_)) { } ~FSStat() { // Throw exception instead in calling thread. if (!got_reply) result.set_exception(make_exception_ptr(runtime_error("Failed stat"))); } void parse_reply() override { uint64_t size = reply_builder.read_u64(); uint32_t type = reply_builder.read_u32(); uint64_t last_modified = reply_builder.read_u64(); FileStat s; s.size = size; s.last_modified = last_modified; switch (type) { case NETFS_FILE_TYPE_PLAIN: s.type = PathType::File; break; case NETFS_FILE_TYPE_DIRECTORY: s.type = PathType::Directory; break; case NETFS_FILE_TYPE_SPECIAL: s.type = PathType::Special; break; } got_reply = true; try { result.set_value(s); } catch (...) { } } std::promise result; bool got_reply = false; }; struct FSWriteCommand : LooperHandler { FSWriteCommand(const string &path, const vector &buffer, unique_ptr socket_) : LooperHandler(move(socket_)) { target_size = buffer.size(); reply_builder.begin(); result_reply.begin(4 * sizeof(uint32_t)); reply_builder.add_u32(NETFS_WRITE_FILE); reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST); reply_builder.add_string(path); reply_builder.add_u32(NETFS_BEGIN_CHUNK_REQUEST); reply_builder.add_u64(buffer.size()); reply_builder.add_buffer(buffer); command_writer.start(reply_builder.get_buffer()); command_reader.start(result_reply.get_buffer()); state = WriteCommand; } bool write_command(Looper &looper, EventFlags flags) { if (flags & EVENT_IN) { auto ret = command_reader.process(*socket); // Received message before we completed the write, must be an error. if (command_reader.complete()) return false; return (ret > 0) || (ret == Socket::ErrorWouldBlock); } else if (flags & EVENT_OUT) { auto ret = command_writer.process(*socket); if (command_writer.complete()) { // Done writing, wait for reply. looper.modify_handler(EVENT_IN, *this); state = ReadReply; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } return true; } ~FSWriteCommand() { if (!got_reply) result.set_exception(make_exception_ptr(runtime_error("Failed write"))); } bool read_reply(Looper &) { auto ret = command_reader.process(*socket); if (command_reader.complete()) { if (result_reply.read_u32() != NETFS_BEGIN_CHUNK_REPLY) return false; if (result_reply.read_u32() != NETFS_ERROR_OK) return false; if (result_reply.read_u64() != target_size) return false; got_reply = true; try { result.set_value(NETFS_ERROR_OK); } catch (...) { } return false; } return (ret > 0) || (ret == Socket::ErrorWouldBlock); } bool handle(Looper &looper, EventFlags flags) override { if (state == WriteCommand) return write_command(looper, flags); else if (state == ReadReply) return read_reply(looper); else return false; } enum State { WriteCommand, ReadReply }; State state = WriteCommand; SocketReader command_reader; SocketWriter command_writer; ReplyBuilder reply_builder; ReplyBuilder result_reply; size_t target_size = 0; promise result; bool got_reply = false; }; NetworkFilesystem::NetworkFilesystem() { looper_thread = thread(&NetworkFilesystem::looper_entry, this); } void NetworkFilesystem::looper_entry() { while (looper.wait_idle(-1) >= 0); } void NetworkFilesystem::setup_notification() { auto socket = Socket::connect(HOST_IP, 7070); if (!socket) return; notify = new FSNotifyCommand(protocol, move(socket)); notify->set_notify_cb([this](const FileNotifyInfo &info) { signal_notification(info); }); // Move capture would be nice ... looper.run_in_looper([this]() { looper.register_handler(EVENT_OUT, unique_ptr(notify)); }); } void NetworkFilesystem::uninstall_notification(FileNotifyHandle handle) { if (!notify) setup_notification(); if (!notify) return; auto itr = handlers.find(handle); if (itr == end(handlers)) return; handlers.erase(itr); auto *value = new promise; auto result = value->get_future(); looper.run_in_looper([this, value, handle]() { notify->push_unregister_notification(handle, move(*value)); delete value; }); try { result.wait(); } catch (...) { } } void NetworkFilesystem::signal_notification(const FileNotifyInfo &info) { lock_guard holder{lock}; pending.push_back(info); } void NetworkFilesystem::poll_notifications() { vector tmp_pending; { lock_guard holder{lock}; swap(tmp_pending, pending); } for (auto ¬ification : tmp_pending) { auto &func = handlers[notification.handle]; if (func) func(notification); } } FileNotifyHandle NetworkFilesystem::install_notification(const std::string &path, std::function func) { if (!notify) setup_notification(); if (!notify) return -1; auto *value = new promise; auto result = value->get_future(); looper.run_in_looper([this, value, path]() { notify->push_register_notification(path, move(*value)); delete value; }); try { auto handle = result.get(); handlers[handle] = move(func); return handle; } catch (...) { return -1; } } vector NetworkFilesystem::list(const std::string &path) { auto joined = protocol + "://" + path; auto socket = Socket::connect(HOST_IP, 7070); if (!socket) return {}; unique_ptr handler(new FSList(joined, move(socket))); auto fut = handler->result.get_future(); looper.run_in_looper([&]() { looper.register_handler(EVENT_OUT, move(handler)); }); try { return fut.get(); } catch (...) { return {}; } } NetworkFile::~NetworkFile() { unmap(); } NetworkFile *NetworkFile::open(Granite::Looper &looper, const std::string &path, Granite::FileMode mode) { auto *file = new NetworkFile; if (!file->init(looper, path, mode)) { delete file; return nullptr; } else return file; } bool NetworkFile::init(Looper &looper_, const std::string &path_, FileMode mode_) { path = path_; mode = mode_; looper = &looper_; if (mode == FileMode::ReadWrite) { LOGE("Unsupported file mode.\n"); return false; } if (mode == FileMode::ReadOnly) { if (!reopen()) { LOGE("Failed to connect to server.\n"); return false; } } return true; } void NetworkFile::unmap() { if (mode == FileMode::WriteOnly && has_buffer && need_flush) { need_flush = false; auto socket = Socket::connect(HOST_IP, 7070); if (!socket) throw runtime_error("Failed to connect to server."); auto handler = unique_ptr(new FSWriteCommand(path, buffer, move(socket))); auto reply = handler->result.get_future(); looper->run_in_looper([&handler, this]() { looper->register_handler(EVENT_OUT | EVENT_IN, move(handler)); }); try { NetFSError error = reply.get(); if (error != NETFS_ERROR_OK) LOGE("Failed to write file: %s\n", path.c_str()); } catch (...) { LOGE("Failed to write file: %s\n", path.c_str()); } } } bool NetworkFile::reopen() { if (mode == FileMode::ReadOnly) { has_buffer = false; auto socket = Socket::connect(HOST_IP, 7070); if (!socket) return false; auto *handler = new FSReader(path, move(socket)); future = handler->result.get_future(); // Capture-by-move would be nice here. looper->run_in_looper([handler, this]() { looper->register_handler(EVENT_OUT, unique_ptr(handler)); }); } return true; } void *NetworkFile::map_write(size_t size) { has_buffer = true; need_flush = true; buffer.resize(size); return buffer.empty() ? nullptr : buffer.data(); } void *NetworkFile::map() { try { if (!has_buffer) { buffer = future.get(); has_buffer = true; } return buffer.empty() ? nullptr : buffer.data(); } catch (...) { return nullptr; } } size_t NetworkFile::get_size() { try { if (!has_buffer) { buffer = future.get(); has_buffer = true; } return buffer.size(); } catch (...) { return 0; } } unique_ptr NetworkFilesystem::open(const std::string &path, FileMode mode) { auto joined = protocol + "://" + path; return unique_ptr(NetworkFile::open(looper, move(joined), mode)); } bool NetworkFilesystem::stat(const std::string &path, FileStat &stat) { auto joined = protocol + "://" + path; auto socket = Socket::connect(HOST_IP, 7070); if (!socket) return false; unique_ptr handler(new FSStat(joined, move(socket))); auto fut = handler->result.get_future(); looper.run_in_looper([&]() { looper.register_handler(EVENT_OUT, move(handler)); }); try { stat = fut.get(); return true; } catch (...) { return false; } } NetworkFilesystem::~NetworkFilesystem() { if (notify) notify->expected_destruction(); looper.kill(); if (looper_thread.joinable()) looper_thread.join(); } }