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