diff --git a/meson.build b/meson.build index de56cf109..4ae9259fa 100644 --- a/meson.build +++ b/meson.build @@ -220,6 +220,7 @@ sources = [ 'src/client/Subscribe.cxx', 'src/client/File.cxx', 'src/client/Response.cxx', + 'src/client/ThreadBackgroundCommand.cxx', 'src/Listen.cxx', 'src/LogInit.cxx', 'src/LogBackend.cxx', diff --git a/src/client/BackgroundCommand.hxx b/src/client/BackgroundCommand.hxx new file mode 100644 index 000000000..8760fddc1 --- /dev/null +++ b/src/client/BackgroundCommand.hxx @@ -0,0 +1,47 @@ +/* + * Copyright 2003-2019 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. + */ + +#ifndef MPD_BACKGROUND_COMMAND_HXX +#define MPD_BACKGROUND_COMMAND_HXX + +/** + * A command running in background. It can take some time to finish, + * and will then call Client::OnBackgroundCommandFinished() from + * inside the client's #EventLoop thread. The important point is that + * sucha long-running command does not block MPD's main loop, and + * other clients can still be handled meanwhile. + * + * (Note: "idle" is not a "background command" by this definition; it + * is a special case.) + * + * @see ThreadBackgroundCommand + */ +class BackgroundCommand { +public: + virtual ~BackgroundCommand() = default; + + /** + * Cancel command execution. After this method returns, the + * object will be deleted. It will be called from the + * #Client's #EventLoop thread. + */ + virtual void Cancel() noexcept = 0; +}; + +#endif diff --git a/src/client/Client.cxx b/src/client/Client.cxx index 2bb326bf8..333346cb6 100644 --- a/src/client/Client.cxx +++ b/src/client/Client.cxx @@ -18,8 +18,10 @@ */ #include "Client.hxx" +#include "Config.hxx" #include "Partition.hxx" #include "Instance.hxx" +#include "BackgroundCommand.hxx" #include "util/Domain.hxx" #include "config.h" @@ -27,6 +29,37 @@ Client::~Client() noexcept { if (FullyBufferedSocket::IsDefined()) FullyBufferedSocket::Close(); + + if (background_command) { + background_command->Cancel(); + background_command.reset(); + } +} + +void +Client::SetBackgroundCommand(std::unique_ptr _bc) noexcept +{ + assert(!background_command); + assert(_bc); + + background_command = std::move(_bc); + + /* disable timeouts while in "idle" */ + timeout_event.Cancel(); +} + +void +Client::OnBackgroundCommandFinished() noexcept +{ + assert(background_command); + + background_command.reset(); + + /* just in case OnSocketInput() has returned + InputResult::PAUSE meanwhile */ + ResumeInput(); + + timeout_event.Schedule(client_timeout); } Instance & diff --git a/src/client/Client.hxx b/src/client/Client.hxx index 753f0a04e..11ee0ed00 100644 --- a/src/client/Client.hxx +++ b/src/client/Client.hxx @@ -34,6 +34,7 @@ #include #include #include +#include #include @@ -47,6 +48,7 @@ class PlayerControl; struct playlist; class Database; class Storage; +class BackgroundCommand; class Client final : FullyBufferedSocket, @@ -102,6 +104,14 @@ private: */ std::list messages; + /** + * The command currently running in background. If this is + * set, then the client is occupied and will not process any + * new input. If the connection gets closed, the + * #BackgroundCommand will be cancelled. + */ + std::unique_ptr background_command; + public: Client(EventLoop &loop, Partition &partition, UniqueSocketDescriptor fd, int uid, @@ -152,6 +162,19 @@ public: void IdleAdd(unsigned flags) noexcept; bool IdleWait(unsigned flags) noexcept; + /** + * Called by a command handler to defer execution to a + * #BackgroundCommand. + */ + void SetBackgroundCommand(std::unique_ptr _bc) noexcept; + + /** + * Called by the current #BackgroundCommand when it has + * finished, after sending the response. This method then + * deletes the #BackgroundCommand. + */ + void OnBackgroundCommandFinished() noexcept; + enum class SubscribeResult { /** success */ OK, diff --git a/src/client/Expire.cxx b/src/client/Expire.cxx index d6129bf5f..b52d780b8 100644 --- a/src/client/Expire.cxx +++ b/src/client/Expire.cxx @@ -25,6 +25,7 @@ void Client::OnTimeout() noexcept { assert(!idle_waiting); + assert(!background_command); FormatDebug(client_domain, "[%u] timeout", num); diff --git a/src/client/New.cxx b/src/client/New.cxx index 9ef1c5233..432e4076b 100644 --- a/src/client/New.cxx +++ b/src/client/New.cxx @@ -21,6 +21,7 @@ #include "Config.hxx" #include "Domain.hxx" #include "List.hxx" +#include "BackgroundCommand.hxx" #include "Partition.hxx" #include "Instance.hxx" #include "net/UniqueSocketDescriptor.hxx" diff --git a/src/client/Process.cxx b/src/client/Process.cxx index e138d2fa3..8966eca05 100644 --- a/src/client/Process.cxx +++ b/src/client/Process.cxx @@ -54,6 +54,8 @@ Client::ProcessCommandList(bool list_ok, CommandResult Client::ProcessLine(char *line) noexcept { + assert(!background_command); + if (!IsLowerAlphaASCII(*line)) { /* all valid MPD commands begin with a lower case letter; this could be a badly routed HTTP diff --git a/src/client/Read.cxx b/src/client/Read.cxx index d7ada1bc1..191ba49f2 100644 --- a/src/client/Read.cxx +++ b/src/client/Read.cxx @@ -29,6 +29,9 @@ BufferedSocket::InputResult Client::OnSocketInput(void *data, size_t length) noexcept { + if (background_command) + return InputResult::PAUSE; + char *p = (char *)data; char *newline = (char *)memchr(p, '\n', length); if (newline == nullptr) @@ -48,6 +51,7 @@ Client::OnSocketInput(void *data, size_t length) noexcept switch (result) { case CommandResult::OK: case CommandResult::IDLE: + case CommandResult::BACKGROUND: case CommandResult::ERROR: break; diff --git a/src/client/ThreadBackgroundCommand.cxx b/src/client/ThreadBackgroundCommand.cxx new file mode 100644 index 000000000..32297c306 --- /dev/null +++ b/src/client/ThreadBackgroundCommand.cxx @@ -0,0 +1,76 @@ +/* + * Copyright 2003-2019 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 "ThreadBackgroundCommand.hxx" +#include "Client.hxx" +#include "Response.hxx" +#include "command/CommandError.hxx" +#include "protocol/Result.hxx" + +ThreadBackgroundCommand::ThreadBackgroundCommand(Client &_client) noexcept + :thread(BIND_THIS_METHOD(_Run)), + defer_finish(_client.GetEventLoop(), BIND_THIS_METHOD(DeferredFinish)), + client(_client) +{ +} + +void +ThreadBackgroundCommand::_Run() noexcept +{ + assert(!error); + + try { + Run(); + } catch (...) { + error = std::current_exception(); + } + + defer_finish.Schedule(); +} + +void +ThreadBackgroundCommand::DeferredFinish() noexcept +{ + /* free the Thread */ + thread.Join(); + + /* send the response */ + Response response(client, 0); + + if (!error) { + PrintError(response, std::move(error)); + } else { + SendResponse(response); + command_success(client); + } + + /* delete this object */ + client.OnBackgroundCommandFinished(); +} + +void +ThreadBackgroundCommand::Cancel() noexcept +{ + CancelThread(); + thread.Join(); + + /* cancel the DeferEvent, just in case the Thread has + meanwhile finished execution */ + defer_finish.Cancel(); +} diff --git a/src/client/ThreadBackgroundCommand.hxx b/src/client/ThreadBackgroundCommand.hxx new file mode 100644 index 000000000..ff55b3bcd --- /dev/null +++ b/src/client/ThreadBackgroundCommand.hxx @@ -0,0 +1,79 @@ +/* + * Copyright 2003-2019 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. + */ + +#ifndef MPD_THREAD_BACKGROUND_COMMAND_HXX +#define MPD_THREAD_BACKGROUND_COMMAND_HXX + +#include "BackgroundCommand.hxx" +#include "event/DeferEvent.hxx" +#include "thread/Thread.hxx" + +#include + +class Client; +class Response; + +/** + * A #BackgroundCommand which defers execution into a new thread. + */ +class ThreadBackgroundCommand : public BackgroundCommand { + Thread thread; + DeferEvent defer_finish; + Client &client; + + /** + * The error thrown by Run(). + */ + std::exception_ptr error; + +public: + explicit ThreadBackgroundCommand(Client &_client) noexcept; + + auto &GetEventLoop() const noexcept { + return defer_finish.GetEventLoop(); + } + + void Start() { + thread.Start(); + } + + void Cancel() noexcept final; + +private: + void _Run() noexcept; + void DeferredFinish() noexcept; + +protected: + /** + * If this method throws, the exception will be converted to a + * MPD response, and SendResponse() will not be called. + */ + virtual void Run() = 0; + + /** + * Send the response after Run() has finished. Note that you + * must not send errors here; if an error occurs, Run() should + * throw an exception instead. + */ + virtual void SendResponse(Response &response) noexcept = 0; + + virtual void CancelThread() noexcept = 0; +}; + +#endif diff --git a/src/command/CommandResult.hxx b/src/command/CommandResult.hxx index 8386cdb8c..745f7da0d 100644 --- a/src/command/CommandResult.hxx +++ b/src/command/CommandResult.hxx @@ -41,6 +41,11 @@ enum class CommandResult { */ IDLE, + /** + * A #BackgroundCommand has been installed. + */ + BACKGROUND, + /** * There was an error. The "ACK" response was sent to the * client.