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
43struct SDL_ProcessData {
44 pid_t pid;
45};
46
47static 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
55static 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
70static 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
85static 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
101static 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
119static 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
154bool 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
380posix_spawn_fail_all:
381 posix_spawn_file_actions_destroy(&fa);
382
383posix_spawn_fail_attr:
384 posix_spawnattr_destroy(&attr);
385
386posix_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
409bool 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
419bool 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
461void 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