1 | /* |
2 | Simple DirectMedia Layer |
3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
4 | |
5 | This software is provided 'as-is', without any express or implied |
6 | warranty. In no event will the authors be held liable for any damages |
7 | arising from the use of this software. |
8 | |
9 | Permission is granted to anyone to use this software for any purpose, |
10 | including commercial applications, and to alter it and redistribute it |
11 | freely, subject to the following restrictions: |
12 | |
13 | 1. The origin of this software must not be misrepresented; you must not |
14 | claim that you wrote the original software. If you use this software |
15 | in a product, an acknowledgment in the product documentation would be |
16 | appreciated but is not required. |
17 | 2. Altered source versions must be plainly marked as such, and must not be |
18 | misrepresented as being the original software. |
19 | 3. This notice may not be removed or altered from any source distribution. |
20 | */ |
21 | #include "SDL_internal.h" |
22 | |
23 | #ifdef SDL_PROCESS_POSIX |
24 | |
25 | #include <dirent.h> |
26 | #include <fcntl.h> |
27 | #include <errno.h> |
28 | #include <signal.h> |
29 | #include <spawn.h> |
30 | #include <stdio.h> |
31 | #include <stdlib.h> |
32 | #include <string.h> |
33 | #include <unistd.h> |
34 | #include <sys/wait.h> |
35 | |
36 | #include "../SDL_sysprocess.h" |
37 | #include "../../io/SDL_iostream_c.h" |
38 | |
39 | |
40 | #define READ_END 0 |
41 | #define WRITE_END 1 |
42 | |
43 | struct SDL_ProcessData { |
44 | pid_t pid; |
45 | }; |
46 | |
47 | static void CleanupStream(void *userdata, void *value) |
48 | { |
49 | SDL_Process *process = (SDL_Process *)value; |
50 | const char *property = (const char *)userdata; |
51 | |
52 | SDL_ClearProperty(process->props, property); |
53 | } |
54 | |
55 | static bool SetupStream(SDL_Process *process, int fd, const char *mode, const char *property) |
56 | { |
57 | // Set the file descriptor to non-blocking mode |
58 | fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); |
59 | |
60 | SDL_IOStream *io = SDL_IOFromFD(fd, true); |
61 | if (!io) { |
62 | return false; |
63 | } |
64 | |
65 | SDL_SetPointerPropertyWithCleanup(SDL_GetIOProperties(io), "SDL.internal.process" , process, CleanupStream, (void *)property); |
66 | SDL_SetPointerProperty(process->props, property, io); |
67 | return true; |
68 | } |
69 | |
70 | static void IgnoreSignal(int sig) |
71 | { |
72 | struct sigaction action; |
73 | |
74 | sigaction(SIGPIPE, NULL, &action); |
75 | #ifdef HAVE_SA_SIGACTION |
76 | if (action.sa_handler == SIG_DFL && (void (*)(int))action.sa_sigaction == SIG_DFL) { |
77 | #else |
78 | if (action.sa_handler == SIG_DFL) { |
79 | #endif |
80 | action.sa_handler = SIG_IGN; |
81 | sigaction(sig, &action, NULL); |
82 | } |
83 | } |
84 | |
85 | static bool CreatePipe(int fds[2]) |
86 | { |
87 | if (pipe(fds) < 0) { |
88 | return false; |
89 | } |
90 | |
91 | // Make sure the pipe isn't accidentally inherited by another thread creating a process |
92 | fcntl(fds[READ_END], F_SETFD, fcntl(fds[READ_END], F_GETFD) | FD_CLOEXEC); |
93 | fcntl(fds[WRITE_END], F_SETFD, fcntl(fds[WRITE_END], F_GETFD) | FD_CLOEXEC); |
94 | |
95 | // Make sure we don't crash if we write when the pipe is closed |
96 | IgnoreSignal(SIGPIPE); |
97 | |
98 | return true; |
99 | } |
100 | |
101 | static bool GetStreamFD(SDL_PropertiesID props, const char *property, int *result) |
102 | { |
103 | SDL_IOStream *io = (SDL_IOStream *)SDL_GetPointerProperty(props, property, NULL); |
104 | if (!io) { |
105 | SDL_SetError("%s is not set" , property); |
106 | return false; |
107 | } |
108 | |
109 | int fd = (int)SDL_GetNumberProperty(SDL_GetIOProperties(io), SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER, -1); |
110 | if (fd < 0) { |
111 | SDL_SetError("%s doesn't have SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER available" , property); |
112 | return false; |
113 | } |
114 | |
115 | *result = fd; |
116 | return true; |
117 | } |
118 | |
119 | static bool AddFileDescriptorCloseActions(posix_spawn_file_actions_t *fa) |
120 | { |
121 | DIR *dir = opendir("/proc/self/fd" ); |
122 | if (dir) { |
123 | struct dirent *entry; |
124 | while ((entry = readdir(dir)) != NULL) { |
125 | int fd = SDL_atoi(entry->d_name); |
126 | if (fd <= STDERR_FILENO) { |
127 | continue; |
128 | } |
129 | |
130 | int flags = fcntl(fd, F_GETFD); |
131 | if (flags < 0 || (flags & FD_CLOEXEC)) { |
132 | continue; |
133 | } |
134 | if (posix_spawn_file_actions_addclose(fa, fd) != 0) { |
135 | closedir(dir); |
136 | return SDL_SetError("posix_spawn_file_actions_addclose failed: %s" , strerror(errno)); |
137 | } |
138 | } |
139 | closedir(dir); |
140 | } else { |
141 | for (int fd = (int)(sysconf(_SC_OPEN_MAX) - 1); fd > STDERR_FILENO; --fd) { |
142 | int flags = fcntl(fd, F_GETFD); |
143 | if (flags < 0 || (flags & FD_CLOEXEC)) { |
144 | continue; |
145 | } |
146 | if (posix_spawn_file_actions_addclose(fa, fd) != 0) { |
147 | return SDL_SetError("posix_spawn_file_actions_addclose failed: %s" , strerror(errno)); |
148 | } |
149 | } |
150 | } |
151 | return true; |
152 | } |
153 | |
154 | bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID props) |
155 | { |
156 | char * const *args = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, NULL); |
157 | SDL_Environment *env = SDL_GetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, SDL_GetEnvironment()); |
158 | char **envp = NULL; |
159 | SDL_ProcessIO stdin_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_NULL); |
160 | SDL_ProcessIO stdout_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_INHERITED); |
161 | SDL_ProcessIO stderr_option = (SDL_ProcessIO)SDL_GetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_INHERITED); |
162 | bool redirect_stderr = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN, false) && |
163 | !SDL_HasProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER); |
164 | int stdin_pipe[2] = { -1, -1 }; |
165 | int stdout_pipe[2] = { -1, -1 }; |
166 | int stderr_pipe[2] = { -1, -1 }; |
167 | int fd = -1; |
168 | |
169 | // Keep the malloc() before exec() so that an OOM won't run a process at all |
170 | envp = SDL_GetEnvironmentVariables(env); |
171 | if (!envp) { |
172 | return false; |
173 | } |
174 | |
175 | SDL_ProcessData *data = SDL_calloc(1, sizeof(*data)); |
176 | if (!data) { |
177 | SDL_free(envp); |
178 | return false; |
179 | } |
180 | process->internal = data; |
181 | |
182 | posix_spawnattr_t attr; |
183 | posix_spawn_file_actions_t fa; |
184 | |
185 | if (posix_spawnattr_init(&attr) != 0) { |
186 | SDL_SetError("posix_spawnattr_init failed: %s" , strerror(errno)); |
187 | goto posix_spawn_fail_none; |
188 | } |
189 | |
190 | if (posix_spawn_file_actions_init(&fa) != 0) { |
191 | SDL_SetError("posix_spawn_file_actions_init failed: %s" , strerror(errno)); |
192 | goto posix_spawn_fail_attr; |
193 | } |
194 | |
195 | // Background processes don't have access to the terminal |
196 | if (process->background) { |
197 | if (stdin_option == SDL_PROCESS_STDIO_INHERITED) { |
198 | stdin_option = SDL_PROCESS_STDIO_NULL; |
199 | } |
200 | if (stdout_option == SDL_PROCESS_STDIO_INHERITED) { |
201 | stdout_option = SDL_PROCESS_STDIO_NULL; |
202 | } |
203 | if (stderr_option == SDL_PROCESS_STDIO_INHERITED) { |
204 | stderr_option = SDL_PROCESS_STDIO_NULL; |
205 | } |
206 | } |
207 | |
208 | switch (stdin_option) { |
209 | case SDL_PROCESS_STDIO_REDIRECT: |
210 | if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, &fd)) { |
211 | goto posix_spawn_fail_all; |
212 | } |
213 | if (posix_spawn_file_actions_adddup2(&fa, fd, STDIN_FILENO) != 0) { |
214 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
215 | goto posix_spawn_fail_all; |
216 | } |
217 | break; |
218 | case SDL_PROCESS_STDIO_APP: |
219 | if (!CreatePipe(stdin_pipe)) { |
220 | goto posix_spawn_fail_all; |
221 | } |
222 | if (posix_spawn_file_actions_adddup2(&fa, stdin_pipe[READ_END], STDIN_FILENO) != 0) { |
223 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
224 | goto posix_spawn_fail_all; |
225 | } |
226 | break; |
227 | case SDL_PROCESS_STDIO_NULL: |
228 | if (posix_spawn_file_actions_addopen(&fa, STDIN_FILENO, "/dev/null" , O_RDONLY, 0) != 0) { |
229 | SDL_SetError("posix_spawn_file_actions_addopen failed: %s" , strerror(errno)); |
230 | goto posix_spawn_fail_all; |
231 | } |
232 | break; |
233 | case SDL_PROCESS_STDIO_INHERITED: |
234 | default: |
235 | break; |
236 | } |
237 | |
238 | switch (stdout_option) { |
239 | case SDL_PROCESS_STDIO_REDIRECT: |
240 | if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDOUT_POINTER, &fd)) { |
241 | goto posix_spawn_fail_all; |
242 | } |
243 | if (posix_spawn_file_actions_adddup2(&fa, fd, STDOUT_FILENO) != 0) { |
244 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
245 | goto posix_spawn_fail_all; |
246 | } |
247 | break; |
248 | case SDL_PROCESS_STDIO_APP: |
249 | if (!CreatePipe(stdout_pipe)) { |
250 | goto posix_spawn_fail_all; |
251 | } |
252 | if (posix_spawn_file_actions_adddup2(&fa, stdout_pipe[WRITE_END], STDOUT_FILENO) != 0) { |
253 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
254 | goto posix_spawn_fail_all; |
255 | } |
256 | break; |
257 | case SDL_PROCESS_STDIO_NULL: |
258 | if (posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, "/dev/null" , O_WRONLY, 0644) != 0) { |
259 | SDL_SetError("posix_spawn_file_actions_addopen failed: %s" , strerror(errno)); |
260 | goto posix_spawn_fail_all; |
261 | } |
262 | break; |
263 | case SDL_PROCESS_STDIO_INHERITED: |
264 | default: |
265 | break; |
266 | } |
267 | |
268 | if (redirect_stderr) { |
269 | if (posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDERR_FILENO) != 0) { |
270 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
271 | goto posix_spawn_fail_all; |
272 | } |
273 | } else { |
274 | switch (stderr_option) { |
275 | case SDL_PROCESS_STDIO_REDIRECT: |
276 | if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDERR_POINTER, &fd)) { |
277 | goto posix_spawn_fail_all; |
278 | } |
279 | if (posix_spawn_file_actions_adddup2(&fa, fd, STDERR_FILENO) != 0) { |
280 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
281 | goto posix_spawn_fail_all; |
282 | } |
283 | break; |
284 | case SDL_PROCESS_STDIO_APP: |
285 | if (!CreatePipe(stderr_pipe)) { |
286 | goto posix_spawn_fail_all; |
287 | } |
288 | if (posix_spawn_file_actions_adddup2(&fa, stderr_pipe[WRITE_END], STDERR_FILENO) != 0) { |
289 | SDL_SetError("posix_spawn_file_actions_adddup2 failed: %s" , strerror(errno)); |
290 | goto posix_spawn_fail_all; |
291 | } |
292 | break; |
293 | case SDL_PROCESS_STDIO_NULL: |
294 | if (posix_spawn_file_actions_addopen(&fa, STDERR_FILENO, "/dev/null" , O_WRONLY, 0644) != 0) { |
295 | SDL_SetError("posix_spawn_file_actions_addopen failed: %s" , strerror(errno)); |
296 | goto posix_spawn_fail_all; |
297 | } |
298 | break; |
299 | case SDL_PROCESS_STDIO_INHERITED: |
300 | default: |
301 | break; |
302 | } |
303 | } |
304 | |
305 | if (!AddFileDescriptorCloseActions(&fa)) { |
306 | goto posix_spawn_fail_all; |
307 | } |
308 | |
309 | // Spawn the new process |
310 | if (process->background) { |
311 | int status = -1; |
312 | #ifdef SDL_PLATFORM_APPLE // Apple has vfork marked as deprecated and (as of macOS 10.12) is almost identical to calling fork() anyhow. |
313 | const pid_t pid = fork(); |
314 | const char *forkname = "fork" ; |
315 | #else |
316 | const pid_t pid = vfork(); |
317 | const char *forkname = "vfork" ; |
318 | #endif |
319 | switch (pid) { |
320 | case -1: |
321 | SDL_SetError("%s() failed: %s" , forkname, strerror(errno)); |
322 | goto posix_spawn_fail_all; |
323 | |
324 | case 0: |
325 | // Detach from the terminal and launch the process |
326 | setsid(); |
327 | if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, envp) != 0) { |
328 | _exit(errno); |
329 | } |
330 | _exit(0); |
331 | |
332 | default: |
333 | if (waitpid(pid, &status, 0) < 0) { |
334 | SDL_SetError("waitpid() failed: %s" , strerror(errno)); |
335 | goto posix_spawn_fail_all; |
336 | } |
337 | if (status != 0) { |
338 | SDL_SetError("posix_spawn() failed: %s" , strerror(status)); |
339 | goto posix_spawn_fail_all; |
340 | } |
341 | break; |
342 | } |
343 | } else { |
344 | if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, envp) != 0) { |
345 | SDL_SetError("posix_spawn() failed: %s" , strerror(errno)); |
346 | goto posix_spawn_fail_all; |
347 | } |
348 | } |
349 | SDL_SetNumberProperty(process->props, SDL_PROP_PROCESS_PID_NUMBER, data->pid); |
350 | |
351 | if (stdin_option == SDL_PROCESS_STDIO_APP) { |
352 | if (!SetupStream(process, stdin_pipe[WRITE_END], "wb" , SDL_PROP_PROCESS_STDIN_POINTER)) { |
353 | close(stdin_pipe[WRITE_END]); |
354 | } |
355 | close(stdin_pipe[READ_END]); |
356 | } |
357 | |
358 | if (stdout_option == SDL_PROCESS_STDIO_APP) { |
359 | if (!SetupStream(process, stdout_pipe[READ_END], "rb" , SDL_PROP_PROCESS_STDOUT_POINTER)) { |
360 | close(stdout_pipe[READ_END]); |
361 | } |
362 | close(stdout_pipe[WRITE_END]); |
363 | } |
364 | |
365 | if (stderr_option == SDL_PROCESS_STDIO_APP) { |
366 | if (!SetupStream(process, stderr_pipe[READ_END], "rb" , SDL_PROP_PROCESS_STDERR_POINTER)) { |
367 | close(stderr_pipe[READ_END]); |
368 | } |
369 | close(stderr_pipe[WRITE_END]); |
370 | } |
371 | |
372 | posix_spawn_file_actions_destroy(&fa); |
373 | posix_spawnattr_destroy(&attr); |
374 | SDL_free(envp); |
375 | |
376 | return true; |
377 | |
378 | /* --------------------------------------------------------------------- */ |
379 | |
380 | posix_spawn_fail_all: |
381 | posix_spawn_file_actions_destroy(&fa); |
382 | |
383 | posix_spawn_fail_attr: |
384 | posix_spawnattr_destroy(&attr); |
385 | |
386 | posix_spawn_fail_none: |
387 | if (stdin_pipe[READ_END] >= 0) { |
388 | close(stdin_pipe[READ_END]); |
389 | } |
390 | if (stdin_pipe[WRITE_END] >= 0) { |
391 | close(stdin_pipe[WRITE_END]); |
392 | } |
393 | if (stdout_pipe[READ_END] >= 0) { |
394 | close(stdout_pipe[READ_END]); |
395 | } |
396 | if (stdout_pipe[WRITE_END] >= 0) { |
397 | close(stdout_pipe[WRITE_END]); |
398 | } |
399 | if (stderr_pipe[READ_END] >= 0) { |
400 | close(stderr_pipe[READ_END]); |
401 | } |
402 | if (stderr_pipe[WRITE_END] >= 0) { |
403 | close(stderr_pipe[WRITE_END]); |
404 | } |
405 | SDL_free(envp); |
406 | return false; |
407 | } |
408 | |
409 | bool SDL_SYS_KillProcess(SDL_Process *process, bool force) |
410 | { |
411 | int ret = kill(process->internal->pid, force ? SIGKILL : SIGTERM); |
412 | if (ret == 0) { |
413 | return true; |
414 | } else { |
415 | return SDL_SetError("Could not kill(): %s" , strerror(errno)); |
416 | } |
417 | } |
418 | |
419 | bool SDL_SYS_WaitProcess(SDL_Process *process, bool block, int *exitcode) |
420 | { |
421 | int wstatus = 0; |
422 | int ret; |
423 | pid_t pid = process->internal->pid; |
424 | |
425 | if (process->background) { |
426 | // We can't wait on the status, so we'll poll to see if it's alive |
427 | if (block) { |
428 | while (kill(pid, 0) == 0) { |
429 | SDL_Delay(10); |
430 | } |
431 | } else { |
432 | if (kill(pid, 0) == 0) { |
433 | return false; |
434 | } |
435 | } |
436 | *exitcode = 0; |
437 | return true; |
438 | } else { |
439 | ret = waitpid(pid, &wstatus, block ? 0 : WNOHANG); |
440 | if (ret < 0) { |
441 | return SDL_SetError("Could not waitpid(): %s" , strerror(errno)); |
442 | } |
443 | |
444 | if (ret == 0) { |
445 | SDL_ClearError(); |
446 | return false; |
447 | } |
448 | |
449 | if (WIFEXITED(wstatus)) { |
450 | *exitcode = WEXITSTATUS(wstatus); |
451 | } else if (WIFSIGNALED(wstatus)) { |
452 | *exitcode = -WTERMSIG(wstatus); |
453 | } else { |
454 | *exitcode = -255; |
455 | } |
456 | |
457 | return true; |
458 | } |
459 | } |
460 | |
461 | void SDL_SYS_DestroyProcess(SDL_Process *process) |
462 | { |
463 | SDL_IOStream *io; |
464 | |
465 | io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDIN_POINTER, NULL); |
466 | if (io) { |
467 | SDL_CloseIO(io); |
468 | } |
469 | |
470 | io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDOUT_POINTER, NULL); |
471 | if (io) { |
472 | SDL_CloseIO(io); |
473 | } |
474 | |
475 | io = (SDL_IOStream *)SDL_GetPointerProperty(process->props, SDL_PROP_PROCESS_STDERR_POINTER, NULL); |
476 | if (io) { |
477 | SDL_CloseIO(io); |
478 | } |
479 | |
480 | SDL_free(process->internal); |
481 | } |
482 | |
483 | #endif // SDL_PROCESS_POSIX |
484 | |