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