/* * Copyright (C) 2003-2015 The Music Player Daemon Project * http://www.musicpd.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "config.h" #include "Connection.hxx" #include "Lease.hxx" #include "Domain.hxx" #include "Callback.hxx" #include "event/Loop.hxx" #include "system/fd_util.h" #include "util/Error.hxx" extern "C" { #include } #include #include /* for POLLIN, POLLOUT */ static constexpr unsigned NFS_MOUNT_TIMEOUT = 60; inline bool NfsConnection::CancellableCallback::Stat(nfs_context *ctx, const char *path, Error &error) { assert(connection.GetEventLoop().IsInside()); int result = nfs_stat_async(ctx, path, Callback, this); if (result < 0) { error.Format(nfs_domain, "nfs_stat_async() failed: %s", nfs_get_error(ctx)); return false; } return true; } inline bool NfsConnection::CancellableCallback::OpenDirectory(nfs_context *ctx, const char *path, Error &error) { assert(connection.GetEventLoop().IsInside()); int result = nfs_opendir_async(ctx, path, Callback, this); if (result < 0) { error.Format(nfs_domain, "nfs_opendir_async() failed: %s", nfs_get_error(ctx)); return false; } return true; } inline bool NfsConnection::CancellableCallback::Open(nfs_context *ctx, const char *path, int flags, Error &error) { assert(connection.GetEventLoop().IsInside()); int result = nfs_open_async(ctx, path, flags, Callback, this); if (result < 0) { error.Format(nfs_domain, "nfs_open_async() failed: %s", nfs_get_error(ctx)); return false; } return true; } inline bool NfsConnection::CancellableCallback::Stat(nfs_context *ctx, struct nfsfh *fh, Error &error) { assert(connection.GetEventLoop().IsInside()); int result = nfs_fstat_async(ctx, fh, Callback, this); if (result < 0) { error.Format(nfs_domain, "nfs_fstat_async() failed: %s", nfs_get_error(ctx)); return false; } return true; } inline bool NfsConnection::CancellableCallback::Read(nfs_context *ctx, struct nfsfh *fh, uint64_t offset, size_t size, Error &error) { assert(connection.GetEventLoop().IsInside()); int result = nfs_pread_async(ctx, fh, offset, size, Callback, this); if (result < 0) { error.Format(nfs_domain, "nfs_pread_async() failed: %s", nfs_get_error(ctx)); return false; } return true; } inline void NfsConnection::CancellableCallback::CancelAndScheduleClose(struct nfsfh *fh) { assert(connection.GetEventLoop().IsInside()); assert(!open); assert(close_fh == nullptr); assert(fh != nullptr); close_fh = fh; Cancel(); } inline void NfsConnection::CancellableCallback::PrepareDestroyContext() { assert(IsCancelled()); if (close_fh != nullptr) { connection.InternalClose(close_fh); close_fh = nullptr; } } inline void NfsConnection::CancellableCallback::Callback(int err, void *data) { assert(connection.GetEventLoop().IsInside()); if (!IsCancelled()) { assert(close_fh == nullptr); NfsCallback &cb = Get(); connection.callbacks.Remove(*this); if (err >= 0) cb.OnNfsCallback((unsigned)err, data); else cb.OnNfsError(Error(nfs_domain, err, (const char *)data)); } else { if (open) { /* a nfs_open_async() call was cancelled - to avoid a memory leak, close the newly allocated file handle immediately */ assert(close_fh == nullptr); if (err >= 0) { struct nfsfh *fh = (struct nfsfh *)data; connection.Close(fh); } } else if (close_fh != nullptr) connection.DeferClose(close_fh); connection.callbacks.Remove(*this); } } void NfsConnection::CancellableCallback::Callback(int err, gcc_unused struct nfs_context *nfs, void *data, void *private_data) { CancellableCallback &c = *(CancellableCallback *)private_data; c.Callback(err, data); } static constexpr unsigned libnfs_to_events(int i) { return ((i & POLLIN) ? SocketMonitor::READ : 0) | ((i & POLLOUT) ? SocketMonitor::WRITE : 0); } static constexpr int events_to_libnfs(unsigned i) { return ((i & SocketMonitor::READ) ? POLLIN : 0) | ((i & SocketMonitor::WRITE) ? POLLOUT : 0); } NfsConnection::~NfsConnection() { assert(GetEventLoop().IsInside()); assert(new_leases.empty()); assert(active_leases.empty()); assert(callbacks.IsEmpty()); assert(deferred_close.empty()); if (context != nullptr) DestroyContext(); } void NfsConnection::AddLease(NfsLease &lease) { assert(GetEventLoop().IsInside()); new_leases.push_back(&lease); DeferredMonitor::Schedule(); } void NfsConnection::RemoveLease(NfsLease &lease) { assert(GetEventLoop().IsInside()); new_leases.remove(&lease); active_leases.remove(&lease); } bool NfsConnection::Stat(const char *path, NfsCallback &callback, Error &error) { assert(GetEventLoop().IsInside()); assert(!callbacks.Contains(callback)); auto &c = callbacks.Add(callback, *this, false); if (!c.Stat(context, path, error)) { callbacks.Remove(c); return false; } ScheduleSocket(); return true; } bool NfsConnection::OpenDirectory(const char *path, NfsCallback &callback, Error &error) { assert(GetEventLoop().IsInside()); assert(!callbacks.Contains(callback)); auto &c = callbacks.Add(callback, *this, true); if (!c.OpenDirectory(context, path, error)) { callbacks.Remove(c); return false; } ScheduleSocket(); return true; } const struct nfsdirent * NfsConnection::ReadDirectory(struct nfsdir *dir) { assert(GetEventLoop().IsInside()); return nfs_readdir(context, dir); } void NfsConnection::CloseDirectory(struct nfsdir *dir) { assert(GetEventLoop().IsInside()); return nfs_closedir(context, dir); } bool NfsConnection::Open(const char *path, int flags, NfsCallback &callback, Error &error) { assert(GetEventLoop().IsInside()); assert(!callbacks.Contains(callback)); auto &c = callbacks.Add(callback, *this, true); if (!c.Open(context, path, flags, error)) { callbacks.Remove(c); return false; } ScheduleSocket(); return true; } bool NfsConnection::Stat(struct nfsfh *fh, NfsCallback &callback, Error &error) { assert(GetEventLoop().IsInside()); assert(!callbacks.Contains(callback)); auto &c = callbacks.Add(callback, *this, false); if (!c.Stat(context, fh, error)) { callbacks.Remove(c); return false; } ScheduleSocket(); return true; } bool NfsConnection::Read(struct nfsfh *fh, uint64_t offset, size_t size, NfsCallback &callback, Error &error) { assert(GetEventLoop().IsInside()); assert(!callbacks.Contains(callback)); auto &c = callbacks.Add(callback, *this, false); if (!c.Read(context, fh, offset, size, error)) { callbacks.Remove(c); return false; } ScheduleSocket(); return true; } void NfsConnection::Cancel(NfsCallback &callback) { callbacks.Cancel(callback); } static void DummyCallback(int, struct nfs_context *, void *, void *) { } inline void NfsConnection::InternalClose(struct nfsfh *fh) { assert(GetEventLoop().IsInside()); assert(context != nullptr); assert(fh != nullptr); nfs_close_async(context, fh, DummyCallback, nullptr); } void NfsConnection::Close(struct nfsfh *fh) { assert(GetEventLoop().IsInside()); InternalClose(fh); ScheduleSocket(); } void NfsConnection::CancelAndClose(struct nfsfh *fh, NfsCallback &callback) { CancellableCallback &cancel = callbacks.Get(callback); cancel.CancelAndScheduleClose(fh); } void NfsConnection::DestroyContext() { assert(GetEventLoop().IsInside()); assert(context != nullptr); #ifndef NDEBUG assert(!in_destroy); in_destroy = true; #endif if (!mount_finished) { assert(TimeoutMonitor::IsActive()); TimeoutMonitor::Cancel(); } /* cancel pending DeferredMonitor that was scheduled to notify new leases */ DeferredMonitor::Cancel(); if (SocketMonitor::IsDefined()) SocketMonitor::Steal(); callbacks.ForEach([](CancellableCallback &c){ c.PrepareDestroyContext(); }); nfs_destroy_context(context); context = nullptr; } inline void NfsConnection::DeferClose(struct nfsfh *fh) { assert(GetEventLoop().IsInside()); assert(in_event); assert(in_service); assert(context != nullptr); assert(fh != nullptr); deferred_close.push_front(fh); } void NfsConnection::ScheduleSocket() { assert(GetEventLoop().IsInside()); assert(context != nullptr); if (!SocketMonitor::IsDefined()) { int _fd = nfs_get_fd(context); if (_fd < 0) return; fd_set_cloexec(_fd, true); SocketMonitor::Open(_fd); } SocketMonitor::Schedule(libnfs_to_events(nfs_which_events(context))); } inline int NfsConnection::Service(unsigned flags) { assert(GetEventLoop().IsInside()); assert(context != nullptr); #ifndef NDEBUG assert(!in_event); in_event = true; assert(!in_service); in_service = true; #endif int result = nfs_service(context, events_to_libnfs(flags)); #ifndef NDEBUG assert(context != nullptr); assert(in_service); in_service = false; #endif return result; } bool NfsConnection::OnSocketReady(unsigned flags) { assert(GetEventLoop().IsInside()); assert(deferred_close.empty()); bool closed = false; const bool was_mounted = mount_finished; if (!mount_finished) /* until the mount is finished, the NFS client may use various sockets, therefore we unregister and re-register it each time */ SocketMonitor::Steal(); const int result = Service(flags); while (!deferred_close.empty()) { InternalClose(deferred_close.front()); deferred_close.pop_front(); } if (!was_mounted && mount_finished) { if (postponed_mount_error.IsDefined()) { DestroyContext(); closed = true; BroadcastMountError(std::move(postponed_mount_error)); } else if (result == 0) BroadcastMountSuccess(); } else if (result < 0) { /* the connection has failed */ Error error; error.Format(nfs_domain, "NFS connection has failed: %s", nfs_get_error(context)); BroadcastError(std::move(error)); DestroyContext(); closed = true; } else if (nfs_get_fd(context) < 0) { /* this happens when rpc_reconnect_requeue() is called after the connection broke, but autoreconnect was disabled - nfs_service() returns 0 */ Error error; const char *msg = nfs_get_error(context); if (msg == nullptr) error.Set(nfs_domain, "NFS socket disappeared"); else error.Format(nfs_domain, "NFS socket disappeared: %s", msg); BroadcastError(std::move(error)); DestroyContext(); closed = true; } assert(context == nullptr || nfs_get_fd(context) >= 0); #ifndef NDEBUG assert(in_event); in_event = false; #endif if (context != nullptr) ScheduleSocket(); return !closed; } inline void NfsConnection::MountCallback(int status, gcc_unused nfs_context *nfs, gcc_unused void *data) { assert(GetEventLoop().IsInside()); assert(context == nfs); mount_finished = true; assert(TimeoutMonitor::IsActive() || in_destroy); TimeoutMonitor::Cancel(); if (status < 0) { postponed_mount_error.Format(nfs_domain, status, "nfs_mount_async() failed: %s", nfs_get_error(context)); return; } } void NfsConnection::MountCallback(int status, nfs_context *nfs, void *data, void *private_data) { NfsConnection *c = (NfsConnection *)private_data; c->MountCallback(status, nfs, data); } inline bool NfsConnection::MountInternal(Error &error) { assert(GetEventLoop().IsInside()); assert(context == nullptr); context = nfs_init_context(); if (context == nullptr) { error.Set(nfs_domain, "nfs_init_context() failed"); return false; } postponed_mount_error.Clear(); mount_finished = false; TimeoutMonitor::ScheduleSeconds(NFS_MOUNT_TIMEOUT); #ifndef NDEBUG in_service = false; in_event = false; in_destroy = false; #endif if (nfs_mount_async(context, server.c_str(), export_name.c_str(), MountCallback, this) != 0) { error.Format(nfs_domain, "nfs_mount_async() failed: %s", nfs_get_error(context)); nfs_destroy_context(context); context = nullptr; return false; } ScheduleSocket(); return true; } void NfsConnection::BroadcastMountSuccess() { assert(GetEventLoop().IsInside()); while (!new_leases.empty()) { auto i = new_leases.begin(); active_leases.splice(active_leases.end(), new_leases, i); (*i)->OnNfsConnectionReady(); } } void NfsConnection::BroadcastMountError(Error &&error) { assert(GetEventLoop().IsInside()); while (!new_leases.empty()) { auto l = new_leases.front(); new_leases.pop_front(); l->OnNfsConnectionFailed(error); } OnNfsConnectionError(std::move(error)); } void NfsConnection::BroadcastError(Error &&error) { assert(GetEventLoop().IsInside()); while (!active_leases.empty()) { auto l = active_leases.front(); active_leases.pop_front(); l->OnNfsConnectionDisconnected(error); } BroadcastMountError(std::move(error)); } void NfsConnection::OnTimeout() { assert(GetEventLoop().IsInside()); assert(!mount_finished); mount_finished = true; DestroyContext(); BroadcastMountError(Error(nfs_domain, "Mount timeout")); } void NfsConnection::RunDeferred() { assert(GetEventLoop().IsInside()); if (context == nullptr) { Error error; if (!MountInternal(error)) { BroadcastMountError(std::move(error)); return; } } if (mount_finished) BroadcastMountSuccess(); }