client/BackgroundCommand: infrastructure for commands running in background

This commit is contained in:
Max Kellermann 2019-04-03 14:31:57 +02:00
parent 28fc1d555f
commit 9f1c23e217
11 changed files with 272 additions and 0 deletions

View File

@ -220,6 +220,7 @@ sources = [
'src/client/Subscribe.cxx', 'src/client/Subscribe.cxx',
'src/client/File.cxx', 'src/client/File.cxx',
'src/client/Response.cxx', 'src/client/Response.cxx',
'src/client/ThreadBackgroundCommand.cxx',
'src/Listen.cxx', 'src/Listen.cxx',
'src/LogInit.cxx', 'src/LogInit.cxx',
'src/LogBackend.cxx', 'src/LogBackend.cxx',

View File

@ -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

View File

@ -18,8 +18,10 @@
*/ */
#include "Client.hxx" #include "Client.hxx"
#include "Config.hxx"
#include "Partition.hxx" #include "Partition.hxx"
#include "Instance.hxx" #include "Instance.hxx"
#include "BackgroundCommand.hxx"
#include "util/Domain.hxx" #include "util/Domain.hxx"
#include "config.h" #include "config.h"
@ -27,6 +29,37 @@ Client::~Client() noexcept
{ {
if (FullyBufferedSocket::IsDefined()) if (FullyBufferedSocket::IsDefined())
FullyBufferedSocket::Close(); FullyBufferedSocket::Close();
if (background_command) {
background_command->Cancel();
background_command.reset();
}
}
void
Client::SetBackgroundCommand(std::unique_ptr<BackgroundCommand> _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 & Instance &

View File

@ -34,6 +34,7 @@
#include <set> #include <set>
#include <string> #include <string>
#include <list> #include <list>
#include <memory>
#include <stddef.h> #include <stddef.h>
@ -47,6 +48,7 @@ class PlayerControl;
struct playlist; struct playlist;
class Database; class Database;
class Storage; class Storage;
class BackgroundCommand;
class Client final class Client final
: FullyBufferedSocket, : FullyBufferedSocket,
@ -102,6 +104,14 @@ private:
*/ */
std::list<ClientMessage> messages; std::list<ClientMessage> 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<BackgroundCommand> background_command;
public: public:
Client(EventLoop &loop, Partition &partition, Client(EventLoop &loop, Partition &partition,
UniqueSocketDescriptor fd, int uid, UniqueSocketDescriptor fd, int uid,
@ -152,6 +162,19 @@ public:
void IdleAdd(unsigned flags) noexcept; void IdleAdd(unsigned flags) noexcept;
bool IdleWait(unsigned flags) noexcept; bool IdleWait(unsigned flags) noexcept;
/**
* Called by a command handler to defer execution to a
* #BackgroundCommand.
*/
void SetBackgroundCommand(std::unique_ptr<BackgroundCommand> _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 { enum class SubscribeResult {
/** success */ /** success */
OK, OK,

View File

@ -25,6 +25,7 @@ void
Client::OnTimeout() noexcept Client::OnTimeout() noexcept
{ {
assert(!idle_waiting); assert(!idle_waiting);
assert(!background_command);
FormatDebug(client_domain, "[%u] timeout", num); FormatDebug(client_domain, "[%u] timeout", num);

View File

@ -21,6 +21,7 @@
#include "Config.hxx" #include "Config.hxx"
#include "Domain.hxx" #include "Domain.hxx"
#include "List.hxx" #include "List.hxx"
#include "BackgroundCommand.hxx"
#include "Partition.hxx" #include "Partition.hxx"
#include "Instance.hxx" #include "Instance.hxx"
#include "net/UniqueSocketDescriptor.hxx" #include "net/UniqueSocketDescriptor.hxx"

View File

@ -54,6 +54,8 @@ Client::ProcessCommandList(bool list_ok,
CommandResult CommandResult
Client::ProcessLine(char *line) noexcept Client::ProcessLine(char *line) noexcept
{ {
assert(!background_command);
if (!IsLowerAlphaASCII(*line)) { if (!IsLowerAlphaASCII(*line)) {
/* all valid MPD commands begin with a lower case /* all valid MPD commands begin with a lower case
letter; this could be a badly routed HTTP letter; this could be a badly routed HTTP

View File

@ -29,6 +29,9 @@
BufferedSocket::InputResult BufferedSocket::InputResult
Client::OnSocketInput(void *data, size_t length) noexcept Client::OnSocketInput(void *data, size_t length) noexcept
{ {
if (background_command)
return InputResult::PAUSE;
char *p = (char *)data; char *p = (char *)data;
char *newline = (char *)memchr(p, '\n', length); char *newline = (char *)memchr(p, '\n', length);
if (newline == nullptr) if (newline == nullptr)
@ -48,6 +51,7 @@ Client::OnSocketInput(void *data, size_t length) noexcept
switch (result) { switch (result) {
case CommandResult::OK: case CommandResult::OK:
case CommandResult::IDLE: case CommandResult::IDLE:
case CommandResult::BACKGROUND:
case CommandResult::ERROR: case CommandResult::ERROR:
break; break;

View File

@ -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();
}

View File

@ -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 <exception>
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

View File

@ -41,6 +41,11 @@ enum class CommandResult {
*/ */
IDLE, IDLE,
/**
* A #BackgroundCommand has been installed.
*/
BACKGROUND,
/** /**
* There was an error. The "ACK" response was sent to the * There was an error. The "ACK" response was sent to the
* client. * client.