1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2018 The Music Player Daemon Project
3 * Project homepage: http://musicpd.org
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "io/Path.hxx"
22 #include "util/Compiler.h"
23 #include "util/ScopeExit.hxx"
24 #include "util/UriUtil.hxx"
26 #include <boost/asio/steady_timer.hpp>
27 #include <boost/asio/posix/stream_descriptor.hpp>
46 /** the pipe to the plugin process */
47 boost::asio::posix::stream_descriptor fd;
49 /** the output of the current plugin */
52 std::array<char, 256> buffer;
54 PluginPipe(boost::asio::io_service &io_service) noexcept
57 ~PluginPipe() noexcept {
61 void AsyncRead() noexcept {
62 fd.async_read_some(boost::asio::buffer(buffer),
63 std::bind(&PluginPipe::OnRead, this,
64 std::placeholders::_1,
65 std::placeholders::_2));
68 void OnRead(const boost::system::error_code &error,
69 std::size_t bytes_transferred) noexcept;
71 void Close() noexcept {
81 /** the plugin list; used for traversing to the next plugin */
84 /** arguments passed to execv() */
85 std::unique_ptr<char *[]> argv;
87 /** caller defined callback function */
88 plugin_callback_t callback;
89 /** caller defined pointer passed to #callback */
92 /** the index of the next plugin which is going to be
94 unsigned next_plugin = 0;
96 /** the pid of the plugin process, or -1 if none is currently
100 /** the stdout pipe */
101 PluginPipe pipe_stdout;
102 /** the stderr pipe */
103 PluginPipe pipe_stderr;
105 /** list of all error messages from failed plugins */
106 std::string all_errors;
108 boost::asio::steady_timer delayed_fail_timer;
110 PluginCycle(boost::asio::io_service &io_service,
111 PluginList &_list, std::unique_ptr<char *[]> &&_argv,
112 plugin_callback_t _callback, void *_callback_data) noexcept
113 :list(&_list), argv(std::move(_argv)),
114 callback(_callback), callback_data(_callback_data),
115 pipe_stdout(io_service), pipe_stderr(io_service),
116 delayed_fail_timer(io_service) {}
118 void TryNextPlugin() noexcept;
120 void ScheduleDelayedFail() noexcept {
121 boost::system::error_code error;
122 delayed_fail_timer.expires_from_now(std::chrono::seconds(0),
124 delayed_fail_timer.async_wait(std::bind(&PluginCycle::OnDelayedFail,
126 std::placeholders::_1));
129 void OnEof() noexcept;
132 int LaunchPlugin(const char *plugin_path) noexcept;
134 void OnDelayedFail(const boost::system::error_code &error) noexcept;
138 register_plugin(PluginList *list, std::string &&path) noexcept
141 if (stat(path.c_str(), &st) < 0)
144 list->plugins.emplace_back(std::move(path));
148 static constexpr bool
149 ShallSkipDirectoryEntry(const char *name) noexcept
151 return name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0));
155 plugin_list_load_directory(PluginList *list, const char *path) noexcept
157 DIR *dir = opendir(path);
161 AtScopeExit(dir) { closedir(dir); };
163 while (const auto *e = readdir(dir)) {
164 const char *name = e->d_name;
165 if (!ShallSkipDirectoryEntry(name))
166 register_plugin(list, BuildPath(path, name));
169 std::sort(list->plugins.begin(), list->plugins.end());
175 PluginCycle::OnEof() noexcept
177 /* Only if both pipes are have EOF status we are done */
178 if (pipe_stdout.fd.is_open() || pipe_stderr.fd.is_open())
181 int status, ret = waitpid(pid, &status, 0);
184 if (ret < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
185 /* If we encountered an error other than service unavailable
186 * (69), log it for later. If all plugins fail, we may get
187 * some hints for debugging.*/
188 if (!pipe_stderr.data.empty() &&
189 WEXITSTATUS(status) != 69) {
190 all_errors += "*** ";
191 all_errors += argv[0];
192 all_errors += " ***\n\n";
193 all_errors += pipe_stderr.data;
197 /* the plugin has failed */
198 pipe_stdout.data.clear();
199 pipe_stderr.data.clear();
203 /* success: invoke the callback */
204 callback(std::move(pipe_stdout.data), true,
205 argv[0], callback_data);
210 PluginPipe::OnRead(const boost::system::error_code &error,
211 std::size_t bytes_transferred) noexcept
214 if (error == boost::asio::error::operation_aborted)
215 /* this object has already been deleted; bail out
216 quickly without touching anything */
224 data.append(&buffer.front(), bytes_transferred);
229 * This is a timer callback which calls the plugin callback "some time
230 * later". This solves the problem that plugin_run() may fail
231 * immediately, leaving its return value in an undefined state.
232 * Instead, install a timer which calls the plugin callback in the
236 PluginCycle::OnDelayedFail(const boost::system::error_code &error) noexcept
241 assert(!pipe_stdout.fd.is_open());
242 assert(!pipe_stderr.fd.is_open());
245 callback(std::move(all_errors), false, nullptr,
250 plugin_fd_add(PluginCycle *cycle, PluginPipe *p, int fd) noexcept
258 PluginCycle::LaunchPlugin(const char *plugin_path) noexcept
261 assert(!pipe_stdout.fd.is_open());
262 assert(!pipe_stderr.fd.is_open());
263 assert(pipe_stdout.data.empty());
264 assert(pipe_stderr.data.empty());
266 /* set new program name, but free the one from the previous
268 argv[0] = const_cast<char *>(GetUriFilename(plugin_path));
271 if (pipe(fds_stdout) < 0)
275 if (pipe(fds_stderr) < 0) {
276 close(fds_stdout[0]);
277 close(fds_stdout[1]);
284 close(fds_stdout[0]);
285 close(fds_stdout[1]);
286 close(fds_stderr[0]);
287 close(fds_stderr[1]);
292 dup2(fds_stdout[1], 1);
293 dup2(fds_stderr[1], 2);
294 close(fds_stdout[0]);
295 close(fds_stdout[1]);
296 close(fds_stderr[0]);
297 close(fds_stderr[1]);
299 /* XXX close other fds? */
301 execv(plugin_path, argv.get());
305 close(fds_stdout[1]);
306 close(fds_stderr[1]);
310 plugin_fd_add(this, &pipe_stdout, fds_stdout[0]);
311 plugin_fd_add(this, &pipe_stderr, fds_stderr[0]);
317 PluginCycle::TryNextPlugin() noexcept
320 assert(!pipe_stdout.fd.is_open());
321 assert(!pipe_stderr.fd.is_open());
322 assert(pipe_stdout.data.empty());
323 assert(pipe_stderr.data.empty());
325 if (next_plugin >= list->plugins.size()) {
326 /* no plugins left */
327 ScheduleDelayedFail();
331 const char *plugin_path = (const char *)
332 list->plugins[next_plugin++].c_str();
333 if (LaunchPlugin(plugin_path) < 0) {
335 ScheduleDelayedFail();
341 make_argv(const char*const* args) noexcept
344 while (args[num] != nullptr)
348 std::unique_ptr<char *[]> result(new char *[num]);
350 char **ret = result.get();
352 /* reserve space for the program name */
355 while (*args != nullptr)
356 *ret++ = const_cast<char *>(*args++);
358 /* end of argument vector */
365 plugin_run(boost::asio::io_service &io_service,
366 PluginList *list, const char *const*args,
367 plugin_callback_t callback, void *callback_data) noexcept
369 assert(args != nullptr);
371 auto *cycle = new PluginCycle(io_service, *list, make_argv(args),
372 callback, callback_data);
373 cycle->TryNextPlugin();
379 plugin_stop(PluginCycle *cycle) noexcept
381 if (cycle->pid > 0) {
382 /* kill the plugin process */
384 cycle->pipe_stdout.Close();
385 cycle->pipe_stderr.Close();
389 kill(cycle->pid, SIGTERM);
390 waitpid(cycle->pid, &status, 0);