conf: allow escaping single quote with backslash in key bindings
[ncmpc-debian.git] / src / plugin.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
4  *
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.
9  *
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.
14  *
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.
18  */
19
20 #include "plugin.hxx"
21 #include "io/Path.hxx"
22 #include "util/Compiler.h"
23 #include "util/ScopeExit.hxx"
24 #include "util/UriUtil.hxx"
25
26 #include <boost/asio/steady_timer.hpp>
27 #include <boost/asio/posix/stream_descriptor.hpp>
28
29 #include <algorithm>
30 #include <memory>
31
32 #include <assert.h>
33 #include <stdlib.h>
34 #include <unistd.h>
35 #include <dirent.h>
36 #include <string.h>
37 #include <signal.h>
38 #include <sys/stat.h>
39 #include <sys/wait.h>
40
41 struct PluginCycle;
42
43 struct PluginPipe {
44         PluginCycle *cycle;
45
46         /** the pipe to the plugin process */
47         boost::asio::posix::stream_descriptor fd;
48
49         /** the output of the current plugin */
50         std::string data;
51
52         std::array<char, 256> buffer;
53
54         PluginPipe(boost::asio::io_service &io_service) noexcept
55                 :fd(io_service) {}
56
57         ~PluginPipe() noexcept {
58                 Close();
59         }
60
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));
66         }
67
68         void OnRead(const boost::system::error_code &error,
69                     std::size_t bytes_transferred) noexcept;
70
71         void Close() noexcept {
72                 if (!fd.is_open())
73                         return;
74
75                 fd.cancel();
76                 fd.close();
77         }
78 };
79
80 struct PluginCycle {
81         /** the plugin list; used for traversing to the next plugin */
82         PluginList *list;
83
84         /** arguments passed to execv() */
85         std::unique_ptr<char *[]> argv;
86
87         /** caller defined callback function */
88         plugin_callback_t callback;
89         /** caller defined pointer passed to #callback */
90         void *callback_data;
91
92         /** the index of the next plugin which is going to be
93             invoked */
94         unsigned next_plugin = 0;
95
96         /** the pid of the plugin process, or -1 if none is currently
97             running */
98         pid_t pid = -1;
99
100         /** the stdout pipe */
101         PluginPipe pipe_stdout;
102         /** the stderr pipe */
103         PluginPipe pipe_stderr;
104
105         /** list of all error messages from failed plugins */
106         std::string all_errors;
107
108         boost::asio::steady_timer delayed_fail_timer;
109
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) {}
117
118         void TryNextPlugin() noexcept;
119
120         void ScheduleDelayedFail() noexcept {
121                 boost::system::error_code error;
122                 delayed_fail_timer.expires_from_now(std::chrono::seconds(0),
123                                                     error);
124                 delayed_fail_timer.async_wait(std::bind(&PluginCycle::OnDelayedFail,
125                                                         this,
126                                                         std::placeholders::_1));
127         }
128
129         void OnEof() noexcept;
130
131 private:
132         int LaunchPlugin(const char *plugin_path) noexcept;
133
134         void OnDelayedFail(const boost::system::error_code &error) noexcept;
135 };
136
137 static bool
138 register_plugin(PluginList *list, std::string &&path) noexcept
139 {
140         struct stat st;
141         if (stat(path.c_str(), &st) < 0)
142                 return false;
143
144         list->plugins.emplace_back(std::move(path));
145         return true;
146 }
147
148 static constexpr bool
149 ShallSkipDirectoryEntry(const char *name) noexcept
150 {
151         return name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0));
152 }
153
154 bool
155 plugin_list_load_directory(PluginList *list, const char *path) noexcept
156 {
157         DIR *dir = opendir(path);
158         if (dir == nullptr)
159                 return false;
160
161         AtScopeExit(dir) { closedir(dir); };
162
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));
167         }
168
169         std::sort(list->plugins.begin(), list->plugins.end());
170
171         return true;
172 }
173
174 void
175 PluginCycle::OnEof() noexcept
176 {
177         /* Only if both pipes are have EOF status we are done */
178         if (pipe_stdout.fd.is_open() || pipe_stderr.fd.is_open())
179                 return;
180
181         int status, ret = waitpid(pid, &status, 0);
182         pid = -1;
183
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;
194                         all_errors += "\n";
195                 }
196
197                 /* the plugin has failed */
198                 pipe_stdout.data.clear();
199                 pipe_stderr.data.clear();
200
201                 TryNextPlugin();
202         } else {
203                 /* success: invoke the callback */
204                 callback(std::move(pipe_stdout.data), true,
205                          argv[0], callback_data);
206         }
207 }
208
209 void
210 PluginPipe::OnRead(const boost::system::error_code &error,
211                    std::size_t bytes_transferred) noexcept
212 {
213         if (error) {
214                 if (error == boost::asio::error::operation_aborted)
215                         /* this object has already been deleted; bail out
216                            quickly without touching anything */
217                         return;
218
219                 fd.close();
220                 cycle->OnEof();
221                 return;
222         }
223
224         data.append(&buffer.front(), bytes_transferred);
225         AsyncRead();
226 }
227
228 /**
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
233  * moment after.
234  */
235 void
236 PluginCycle::OnDelayedFail(const boost::system::error_code &error) noexcept
237 {
238         if (error)
239                 return;
240
241         assert(!pipe_stdout.fd.is_open());
242         assert(!pipe_stderr.fd.is_open());
243         assert(pid < 0);
244
245         callback(std::move(all_errors), false, nullptr,
246                  callback_data);
247 }
248
249 static void
250 plugin_fd_add(PluginCycle *cycle, PluginPipe *p, int fd) noexcept
251 {
252         p->cycle = cycle;
253         p->fd.assign(fd);
254         p->AsyncRead();
255 }
256
257 int
258 PluginCycle::LaunchPlugin(const char *plugin_path) noexcept
259 {
260         assert(pid < 0);
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());
265
266         /* set new program name, but free the one from the previous
267            plugin */
268         argv[0] = const_cast<char *>(GetUriFilename(plugin_path));
269
270         int fds_stdout[2];
271         if (pipe(fds_stdout) < 0)
272                 return -1;
273
274         int fds_stderr[2];
275         if (pipe(fds_stderr) < 0) {
276                 close(fds_stdout[0]);
277                 close(fds_stdout[1]);
278                 return -1;
279         }
280
281         pid = fork();
282
283         if (pid < 0) {
284                 close(fds_stdout[0]);
285                 close(fds_stdout[1]);
286                 close(fds_stderr[0]);
287                 close(fds_stderr[1]);
288                 return -1;
289         }
290
291         if (pid == 0) {
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]);
298                 close(0);
299                 /* XXX close other fds? */
300
301                 execv(plugin_path, argv.get());
302                 _exit(1);
303         }
304
305         close(fds_stdout[1]);
306         close(fds_stderr[1]);
307
308         /* XXX CLOEXEC? */
309
310         plugin_fd_add(this, &pipe_stdout, fds_stdout[0]);
311         plugin_fd_add(this, &pipe_stderr, fds_stderr[0]);
312
313         return 0;
314 }
315
316 void
317 PluginCycle::TryNextPlugin() noexcept
318 {
319         assert(pid < 0);
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());
324
325         if (next_plugin >= list->plugins.size()) {
326                 /* no plugins left */
327                 ScheduleDelayedFail();
328                 return;
329         }
330
331         const char *plugin_path = (const char *)
332                 list->plugins[next_plugin++].c_str();
333         if (LaunchPlugin(plugin_path) < 0) {
334                 /* system error */
335                 ScheduleDelayedFail();
336                 return;
337         }
338 }
339
340 static auto
341 make_argv(const char*const* args) noexcept
342 {
343         unsigned num = 0;
344         while (args[num] != nullptr)
345                 ++num;
346         num += 2;
347
348         std::unique_ptr<char *[]> result(new char *[num]);
349
350         char **ret = result.get();
351
352         /* reserve space for the program name */
353         *ret++ = nullptr;
354
355         while (*args != nullptr)
356                 *ret++ = const_cast<char *>(*args++);
357
358         /* end of argument vector */
359         *ret++ = nullptr;
360
361         return result;
362 }
363
364 PluginCycle *
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
368 {
369         assert(args != nullptr);
370
371         auto *cycle = new PluginCycle(io_service, *list, make_argv(args),
372                                       callback, callback_data);
373         cycle->TryNextPlugin();
374
375         return cycle;
376 }
377
378 void
379 plugin_stop(PluginCycle *cycle) noexcept
380 {
381         if (cycle->pid > 0) {
382                 /* kill the plugin process */
383
384                 cycle->pipe_stdout.Close();
385                 cycle->pipe_stderr.Close();
386
387                 int status;
388
389                 kill(cycle->pid, SIGTERM);
390                 waitpid(cycle->pid, &status, 0);
391         }
392
393         delete cycle;
394 }