SDL: Added SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN

From 76c469910eef3574dd2235684533ba68a541e1e3 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 14 Sep 2024 09:13:37 -0700
Subject: [PATCH] Added SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN

---
 include/SDL3/SDL_process.h               | 12 ++-
 src/misc/unix/SDL_sysurl.c               | 85 ++++++++++-----------
 src/process/SDL_process.c                |  5 ++
 src/process/SDL_sysprocess.h             |  1 +
 src/process/posix/SDL_posixprocess.c     | 97 +++++++++++++++++++-----
 src/process/windows/SDL_windowsprocess.c | 14 ++++
 6 files changed, 145 insertions(+), 69 deletions(-)

diff --git a/include/SDL3/SDL_process.h b/include/SDL3/SDL_process.h
index 609c4c4df4ad9..2af0bdcc91ad4 100644
--- a/include/SDL3/SDL_process.h
+++ b/include/SDL3/SDL_process.h
@@ -175,6 +175,7 @@ typedef enum SDL_ProcessIO
  *   output of the process should be redirected into the standard output of
  *   the process. This property has no effect if
  *   `SDL_PROP_PROCESS_CREATE_STDERR_NUMBER` is set.
+ * - `SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN`: true if the process should run in the background. In this case the default input and output is `SDL_PROCESS_STDIO_NULL` and the exitcode of the process is not available, and will always be 0.
  *
  * On POSIX platforms, wait() and waitpid(-1, ...) should not be called, and
  * SIGCHLD should not be ignored or handled because those would prevent SDL
@@ -208,6 +209,7 @@ extern SDL_DECLSPEC SDL_Process *SDLCALL SDL_CreateProcessWithProperties(SDL_Pro
 #define SDL_PROP_PROCESS_CREATE_STDERR_NUMBER               "SDL.process.create.stderr_option"
 #define SDL_PROP_PROCESS_CREATE_STDERR_POINTER              "SDL.process.create.stderr_source"
 #define SDL_PROP_PROCESS_CREATE_STDERR_TO_STDOUT_BOOLEAN    "SDL.process.create.stderr_to_stdout"
+#define SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN          "SDL.process.create.background"
 
 /**
  * Get the properties associated with a process.
@@ -218,6 +220,7 @@ extern SDL_DECLSPEC SDL_Process *SDLCALL SDL_CreateProcessWithProperties(SDL_Pro
  * - `SDL_PROP_PROCESS_STDIN_POINTER`: an SDL_IOStream that can be used to write input to the process, if it was created with `SDL_PROP_PROCESS_CREATE_STDIN_NUMBER` set to `SDL_PROCESS_STDIO_APP`.
  * - `SDL_PROP_PROCESS_STDOUT_POINTER`: a non-blocking SDL_IOStream that can be used to read output from the process, if it was created with `SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER` set to `SDL_PROCESS_STDIO_APP`.
  * - `SDL_PROP_PROCESS_STDERR_POINTER`: a non-blocking SDL_IOStream that can be used to read error output from the process, if it was created with `SDL_PROP_PROCESS_CREATE_STDERR_NUMBER` set to `SDL_PROCESS_STDIO_APP`.
+ * - `SDL_PROP_PROCESS_BACKGROUND_BOOLEAN`: true if the process is running in the background.
  *
  * \param process the process to query.
  * \returns a valid property ID on success or 0 on failure; call
@@ -232,10 +235,11 @@ extern SDL_DECLSPEC SDL_Process *SDLCALL SDL_CreateProcessWithProperties(SDL_Pro
  */
 extern SDL_DECLSPEC SDL_PropertiesID SDL_GetProcessProperties(SDL_Process *process);
 
-#define SDL_PROP_PROCESS_PID_NUMBER     "SDL.process.pid"
-#define SDL_PROP_PROCESS_STDIN_POINTER  "SDL.process.stdin"
-#define SDL_PROP_PROCESS_STDOUT_POINTER "SDL.process.stdout"
-#define SDL_PROP_PROCESS_STDERR_POINTER "SDL.process.stderr"
+#define SDL_PROP_PROCESS_PID_NUMBER         "SDL.process.pid"
+#define SDL_PROP_PROCESS_STDIN_POINTER      "SDL.process.stdin"
+#define SDL_PROP_PROCESS_STDOUT_POINTER     "SDL.process.stdout"
+#define SDL_PROP_PROCESS_STDERR_POINTER     "SDL.process.stderr"
+#define SDL_PROP_PROCESS_BACKGROUND_BOOLEAN "SDL.process.background"
 
 /**
  * Read all the output from a process.
diff --git a/src/misc/unix/SDL_sysurl.c b/src/misc/unix/SDL_sysurl.c
index 681c0deccc12a..69517f1627208 100644
--- a/src/misc/unix/SDL_sysurl.c
+++ b/src/misc/unix/SDL_sysurl.c
@@ -35,51 +35,44 @@ extern char **environ;
 
 bool SDL_SYS_OpenURL(const char *url)
 {
-    const pid_t pid1 = fork();
-    if (pid1 == 0) { // child process
-#ifdef USE_POSIX_SPAWN
-        pid_t pid2;
-        const char *args[] = { "xdg-open", url, NULL };
-        // Clear LD_PRELOAD so Chrome opens correctly when this application is launched by Steam
-        SDL_unsetenv_unsafe("LD_PRELOAD");
-        if (posix_spawnp(&pid2, args[0], NULL, NULL, (char **)args, environ) == 0) {
-            // Child process doesn't wait for possibly-blocking grandchild.
-            _exit(EXIT_SUCCESS);
-        } else {
-            _exit(EXIT_FAILURE);
-        }
-#else
-        pid_t pid2;
-        // Clear LD_PRELOAD so Chrome opens correctly when this application is launched by Steam
-        SDL_unsetenv_unsafe("LD_PRELOAD");
-        // Notice this is vfork and not fork!
-        pid2 = vfork();
-        if (pid2 == 0) { // Grandchild process will try to launch the url
-            execlp("xdg-open", "xdg-open", url, NULL);
-            _exit(EXIT_FAILURE);
-        } else if (pid2 < 0) { // There was an error forking
-            _exit(EXIT_FAILURE);
-        } else {
-            // Child process doesn't wait for possibly-blocking grandchild.
-            _exit(EXIT_SUCCESS);
-        }
-#endif // USE_POSIX_SPAWN
-    } else if (pid1 < 0) {
-        return SDL_SetError("fork() failed: %s", strerror(errno));
-    } else {
-        int status;
-        if (waitpid(pid1, &status, 0) == pid1) {
-            if (WIFEXITED(status)) {
-                if (WEXITSTATUS(status) == 0) {
-                    return true; // success!
-                } else {
-                    return SDL_SetError("xdg-open reported error or failed to launch: %d", WEXITSTATUS(status));
-                }
-            } else {
-                return SDL_SetError("xdg-open failed for some reason");
-            }
-        } else {
-            return SDL_SetError("Waiting on xdg-open failed: %s", strerror(errno));
-        }
+    SDL_Environment *env = NULL;
+    char **process_env = NULL;
+    const char *process_args[] = { "xdg-open", url, NULL };
+    SDL_Process *process = NULL;
+    bool result = false;
+
+    env = SDL_CreateEnvironment(SDL_FALSE);
+    if (!env) {
+        goto done;
+    }
+
+    // Clear LD_PRELOAD so Chrome opens correctly when this application is launched by Steam
+    SDL_UnsetEnvironmentVariable(env, "LD_PRELOAD");
+
+    process_env = SDL_GetEnvironmentVariables(env);
+    if (!process_env) {
+        goto done;
     }
+
+    SDL_PropertiesID props = SDL_CreateProperties();
+    if (!props) {
+        goto done;
+    }
+    SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, process_args);
+    SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, process_env);
+    SDL_SetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, true);
+    process = SDL_CreateProcessWithProperties(props);
+    SDL_DestroyProperties(props);
+    if (!process) {
+        goto done;
+    }
+
+    result = true;
+
+done:
+    SDL_free(process_env);
+    SDL_DestroyEnvironment(env);
+    SDL_DestroyProcess(process);
+
+    return result;
 }
diff --git a/src/process/SDL_process.c b/src/process/SDL_process.c
index 10187e4aa934b..4f0ca27120f60 100644
--- a/src/process/SDL_process.c
+++ b/src/process/SDL_process.c
@@ -54,12 +54,14 @@ SDL_Process *SDL_CreateProcessWithProperties(SDL_PropertiesID props)
     if (!process) {
         return NULL;
     }
+    process->background = SDL_GetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, false);
 
     process->props = SDL_CreateProperties();
     if (!process->props) {
         SDL_DestroyProcess(process);
         return NULL;
     }
+    SDL_SetBooleanProperty(process->props, SDL_PROP_PROCESS_BACKGROUND_BOOLEAN, process->background);
 
     if (!SDL_SYS_CreateProcessWithProperties(process, props)) {
         SDL_DestroyProcess(process);
@@ -180,6 +182,9 @@ SDL_bool SDL_WaitProcess(SDL_Process *process, SDL_bool block, int *exitcode)
     if (SDL_SYS_WaitProcess(process, block, &process->exitcode)) {
         process->alive = false;
         if (exitcode) {
+            if (process->background) {
+                process->exitcode = 0;
+            }
             *exitcode = process->exitcode;
         }
         return true;
diff --git a/src/process/SDL_sysprocess.h b/src/process/SDL_sysprocess.h
index bcc8be4dcec9e..fbea4a679f6e2 100644
--- a/src/process/SDL_sysprocess.h
+++ b/src/process/SDL_sysprocess.h
@@ -25,6 +25,7 @@ typedef struct SDL_ProcessData SDL_ProcessData;
 struct SDL_Process
 {
     bool alive;
+    bool background;
     int exitcode;
     SDL_PropertiesID props;
     SDL_ProcessData *internal;
diff --git a/src/process/posix/SDL_posixprocess.c b/src/process/posix/SDL_posixprocess.c
index 35cc6a5dbd4fa..3aaef3cef27e8 100644
--- a/src/process/posix/SDL_posixprocess.c
+++ b/src/process/posix/SDL_posixprocess.c
@@ -137,6 +137,19 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
         goto posix_spawn_fail_attr;
     }
 
+    // Background processes don't have access to the terminal
+    if (process->background) {
+        if (stdin_option == SDL_PROCESS_STDIO_INHERITED) {
+            stdin_option = SDL_PROCESS_STDIO_NULL;
+        }
+        if (stdout_option == SDL_PROCESS_STDIO_INHERITED) {
+            stdout_option = SDL_PROCESS_STDIO_NULL;
+        }
+        if (stderr_option == SDL_PROCESS_STDIO_INHERITED) {
+            stderr_option = SDL_PROCESS_STDIO_NULL;
+        }
+    }
+
     switch (stdin_option) {
     case SDL_PROCESS_STDIO_REDIRECT:
         if (!GetStreamFD(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, &fd)) {
@@ -276,9 +289,38 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
     }
 
     // Spawn the new process
-    if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, env) != 0) {
-        SDL_SetError("posix_spawn failed: %s", strerror(errno));
-        goto posix_spawn_fail_all;
+    if (process->background) {
+        int status = -1;
+        pid_t pid = vfork();
+        switch (pid) {
+        case -1:
+            SDL_SetError("vfork() failed: %s", strerror(errno));
+            goto posix_spawn_fail_all;
+
+        case 0:
+            // Detach from the terminal and launch the process
+            setsid();
+            if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, env) != 0) {
+                _exit(errno);
+            }
+            _exit(0);
+
+        default:
+            if (waitpid(pid, &status, 0) < 0) {
+                SDL_SetError("waitpid() failed: %s", strerror(errno));
+                goto posix_spawn_fail_all;
+            }
+            if (status != 0) {
+                SDL_SetError("posix_spawn() failed: %s", strerror(status));
+                goto posix_spawn_fail_all;
+            }
+            break;
+        }
+    } else {
+        if (posix_spawnp(&data->pid, args[0], &fa, &attr, args, env) != 0) {
+            SDL_SetError("posix_spawn() failed: %s", strerror(errno));
+            goto posix_spawn_fail_all;
+        }
     }
     SDL_SetNumberProperty(process->props, SDL_PROP_PROCESS_PID_NUMBER, data->pid);
 
@@ -353,26 +395,43 @@ bool SDL_SYS_KillProcess(SDL_Process *process, SDL_bool force)
 bool SDL_SYS_WaitProcess(SDL_Process *process, SDL_bool block, int *exitcode)
 {
     int wstatus = 0;
-    int ret = waitpid(process->internal->pid, &wstatus, block ? 0 : WNOHANG);
+    int ret;
+    pid_t pid = process->internal->pid;
+
+    if (process->background) {
+        // We can't wait on the status, so we'll poll to see if it's alive
+        if (block) {
+            while (kill(pid, 0) == 0) {
+                SDL_Delay(10);
+            }
+        } else {
+            if (kill(pid, 0) == 0) {
+                return false;
+            }
+        }
+        *exitcode = 0;
+        return true;
+    } else {
+        ret = waitpid(pid, &wstatus, block ? 0 : WNOHANG);
+        if (ret < 0) {
+            return SDL_SetError("Could not waitpid(): %s", strerror(errno));
+        }
 
-    if (ret < 0) {
-        return SDL_SetError("Could not waitpid(): %s", strerror(errno));
-    }
+        if (ret == 0) {
+            SDL_ClearError();
+            return false;
+        }
 
-    if (ret == 0) {
-        SDL_ClearError();
-        return false;
-    }
+        if (WIFEXITED(wstatus)) {
+            *exitcode = WEXITSTATUS(wstatus);
+        } else if (WIFSIGNALED(wstatus)) {
+            *exitcode = -WTERMSIG(wstatus);
+        } else {
+            *exitcode = -255;
+        }
 
-    if (WIFEXITED(wstatus)) {
-        *exitcode = WEXITSTATUS(wstatus);
-    } else if (WIFSIGNALED(wstatus)) {
-        *exitcode = -WTERMSIG(wstatus);
-    } else {
-        *exitcode = -255;
+        return true;
     }
-
-    return true;
 }
 
 void SDL_SYS_DestroyProcess(SDL_Process *process)
diff --git a/src/process/windows/SDL_windowsprocess.c b/src/process/windows/SDL_windowsprocess.c
index 75293346253da..3820a578cb068 100644
--- a/src/process/windows/SDL_windowsprocess.c
+++ b/src/process/windows/SDL_windowsprocess.c
@@ -232,6 +232,20 @@ bool SDL_SYS_CreateProcessWithProperties(SDL_Process *process, SDL_PropertiesID
     security_attributes.bInheritHandle = TRUE;
     security_attributes.lpSecurityDescriptor = NULL;
 
+    // Background processes don't have access to the terminal
+    // This isn't necessary on Windows, but we keep the same behavior as the POSIX implementation.
+    if (process->background) {
+        if (stdin_option == SDL_PROCESS_STDIO_INHERITED) {
+            stdin_option = SDL_PROCESS_STDIO_NULL;
+        }
+        if (stdout_option == SDL_PROCESS_STDIO_INHERITED) {
+            stdout_option = SDL_PROCESS_STDIO_NULL;
+        }
+        if (stderr_option == SDL_PROCESS_STDIO_INHERITED) {
+            stderr_option = SDL_PROCESS_STDIO_NULL;
+        }
+    }
+
     switch (stdin_option) {
     case SDL_PROCESS_STDIO_REDIRECT:
         if (!SetupRedirect(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, &startup_info.hStdInput)) {