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