Repository: godotengine/godot (language: cpp)

Issue:
Unit tests fail under `lldb` supervision
### Tested versions

a210fe6dbd27cc01bffacb0ce2816e0cf303af5e / 4.0+

### System information

Godot v4.4.1.stable - macOS Sequoia (15.3.0) - Multi-window, 1 monitor - Metal (Forward+) - integrated Apple M1 Max (Apple7) - Apple M1 Max (10 threads)

### Issue description

When using `lldb`, `test_os.h` fails `[OS] Execute`.
This is a regression from https://github.com/godotengine/godot/pull/42069 (4.0), cc @Calinou.

### Steps to reproduce

1. Be on macOS (may fail on linux too)
2. run  `❯ lldb bin/godot.macos.editor.arm64 -- --test`
3. run `run`
4. Observe the error:

```
./tests/core/os/test_os.h:199: ERROR: CHECK( exit_code == 0 ) is NOT correct!
  values: CHECK( 230 == 0 )
  logged: Running the command `sh -c "ls > /dev/null"` returns a zero (successful) exit code.

Process 42405 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x1005063b0)
    frame #0: 0x00000001005063b0 godot.macos.editor.arm64.master`TestOS::DOCTEST_ANON_FUNC_4825() at test_os.h:197:2 [opt]
   194          CHECK_MESSAGE(
   195                          err == OK,
   196                          "(Running the command `sh -c \"ls > /dev/null\"` returns the expected Godot error code (OK).");
-> 197          CHECK_MESSAGE(
   198                          exit_code == 0,
   199                          "Running the command `sh -c \"ls > /dev/null\"` returns a zero (successful) exit code.");
   200  #endif
Target 0: (godot.macos.editor.arm64.master) stopped.
warning: godot.macos.editor.arm64.master was compiled with optimization - stepping may behave oddly; variables may not be available.
```

The rest of the unit tests run through, so deleting the test is a temporary fix.

### Minimal reproduction project (MRP)

N/A


Discussion / hints:
I don't really know how this works, but I've tested this on a WSL Environment and this is what I got:
```
Process 38477 launched: '/home/seif-eldin/repos/godot/bin/godot.linuxbsd.editor.dev.x86_64.llvm' (x86_64)
Process 38477 stopped and restarted: thread 1 received signal: SIGCHLD
[doctest] doctest version is "2.4.11"
[doctest] run with "--help" for options
tERROR: Unrecognized binary resource file: 'res://gltf_placed_in_dot_godot_imported.scn'.
   at: open (./core/io/resource_format_binary.cpp:1002)
ERROR: EditorNode doesn't exist.
   at: make_scene_preview (./editor/editor_interface.cpp:236)
ERROR: Unrecognized binary resource file: 'res://gltf_pointing_to_texture_outside_of_res_folder.scn'.
   at: open (./core/io/resource_format_binary.cpp:1002)
ERROR: EditorNode doesn't exist.
   at: make_scene_preview (./editor/editor_interface.cpp:236)
ERROR: Unrecognized binary resource file: 'res://embedded_texture.scn'.
   at: open (./core/io/resource_format_binary.cpp:1002)
ERROR: EditorNode doesn't exist.
   at: make_scene_preview (./editor/editor_interface.cpp:236)
Process 38477 stopped and restarted: thread 1 received signal: SIGCHLD
===============================================================================
[doctest] test cases:    1216 |    1216 passed | 0 failed | 3 skipped
[doctest] assertions: 2441777 | 2441777 passed | 0 failed |
[doctest] Status: SUCCESS!
Process 38477 exited with status = 0 (0x00000000) 
```
So probably MacOS only
I can reproduce it, but not sure what's the reason, seems like `fork` fail under LLDB.

Fix patch:
diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp
index d3e4ca92defb..10f8b0ba38a3 100644
--- a/drivers/unix/os_unix.cpp
+++ b/drivers/unix/os_unix.cpp
@@ -805,6 +805,57 @@ Dictionary OS_Unix::execute_with_pipe(const String &p_path, const List<String> &
 #endif
 }
 
+int OS_Unix::_wait_for_pid_completion(const pid_t p_pid, int *r_status, int p_options) {
+	while (true) {
+		if (waitpid(p_pid, r_status, p_options) != -1) {
+			// Thread exited normally.
+			return 0;
+		}
+		const int error = errno;
+		if (error == EINTR) {
+			// We're in a debugger, should call waitpid again.
+			// See https://stackoverflow.com/a/45472920/730797.
+			continue;
+		}
+		return error;
+	}
+}
+
+bool OS_Unix::_check_pid_is_running(const pid_t p_pid, int *r_status) const {
+	const ProcessInfo *pi = process_map->getptr(p_pid);
+
+	if (pi && !pi->is_running) {
+		// Can return cached value.
+		if (r_status) {
+			*r_status = pi->exit_code;
+		}
+		return false;
+	}
+
+	int status = 0;
+	const int result = _wait_for_pid_completion(p_pid, &status, WNOHANG);
+	if (result == 0) {
+		// Thread is still running.
+		return true;
+	}
+
+	ERR_FAIL_COND_V_MSG(result == -1, false, vformat("Thread %d exited with errno: %d", (int)p_pid, errno));
+	// Thread exited normally.
+
+	status = WIFEXITED(status) ? WEXITSTATUS(status) : status;
+
+	if (pi) {
+		pi->is_running = false;
+		pi->exit_code = status;
+	}
+
+	if (r_status) {
+		*r_status = status;
+	}
+
+	return false;
+}
+
 Error OS_Unix::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) {
 #ifdef __EMSCRIPTEN__
 	// Don't compile this code at all to avoid undefined references.
@@ -870,12 +921,12 @@ Error OS_Unix::execute(const String &p_path, const List<String> &p_arguments, St
 		raise(SIGKILL);
 	}
 
-	int status;
-	waitpid(pid, &status, 0);
+	int status = 0;
+	const int result = _wait_for_pid_completion(pid, &status, 0);
 	if (r_exitcode) {
 		*r_exitcode = WIFEXITED(status) ? WEXITSTATUS(status) : status;
 	}
-	return OK;
+	return result ? FAILED : OK;
 #endif
 }
 
@@ -929,7 +980,7 @@ Error OS_Unix::kill(const ProcessID &p_pid) {
 	if (!ret) {
 		//avoid zombie process
 		int st;
-		::waitpid(p_pid, &st, 0);
+		_wait_for_pid_completion(p_pid, &st, 0);
 	}
 	return ret ? ERR_INVALID_PARAMETER : OK;
 }
@@ -940,42 +991,19 @@ int OS_Unix::get_process_id() const {
 
 bool OS_Unix::is_process_running(const ProcessID &p_pid) const {
 	MutexLock lock(process_map_mutex);
-	const ProcessInfo *pi = process_map->getptr(p_pid);
-
-	if (pi && !pi->is_running) {
-		return false;
-	}
-
-	int status = 0;
-	if (waitpid(p_pid, &status, WNOHANG) != 0) {
-		if (pi) {
-			pi->is_running = false;
-			pi->exit_code = status;
-		}
-		return false;
-	}
-
-	return true;
+	return _check_pid_is_running(p_pid, nullptr);
 }
 
 int OS_Unix::get_process_exit_code(const ProcessID &p_pid) const {
 	MutexLock lock(process_map_mutex);
-	const ProcessInfo *pi = process_map->getptr(p_pid);
 
-	if (pi && !pi->is_running) {
-		return pi->exit_code;
+	int exit_code = 0;
+	if (_check_pid_is_running(p_pid, &exit_code)) {
+		// Thread is still running
+		return -1;
 	}
 
-	int status = 0;
-	if (waitpid(p_pid, &status, WNOHANG) != 0) {
-		status = WIFEXITED(status) ? WEXITSTATUS(status) : status;
-		if (pi) {
-			pi->is_running = false;
-			pi->exit_code = status;
-		}
-		return status;
-	}
-	return -1;
+	return exit_code;
 }
 
 String OS_Unix::get_locale() const {
diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h
index 8fde52b5ff00..febc7c79cd89 100644
--- a/drivers/unix/os_unix.h
+++ b/drivers/unix/os_unix.h
@@ -71,6 +71,9 @@ class OS_Unix : public OS {
 	void _load_iconv();
 #endif
 
+	static int _wait_for_pid_completion(const pid_t p_pid, int *r_status, int p_options);
+	bool _check_pid_is_running(const pid_t p_pid, int *r_status) const;
+
 protected:
 	// UNIX only handles the core functions.
 	// inheriting platforms under unix (eg. X11) should handle the rest


Test patch: