| /****************************************************************************** | |
| * Remote Debugging Module - Subprocess Enumeration | |
| * | |
| * This file contains platform-specific functions for enumerating child | |
| * processes of a given PID. | |
| ******************************************************************************/ | |
| #include "_remote_debugging.h" | |
| #include "pycore_fileutils.h" // _Py_strerror() | |
| #ifndef MS_WINDOWS | |
| #include <unistd.h> | |
| #include <dirent.h> | |
| #endif | |
| #ifdef MS_WINDOWS | |
| #include <tlhelp32.h> | |
| #endif | |
| /* ============================================================================ | |
| * INTERNAL DATA STRUCTURES | |
| * ============================================================================ */ | |
| /* Simple dynamic array for collecting PIDs */ | |
| typedef struct { | |
| pid_t *pids; | |
| size_t count; | |
| size_t capacity; | |
| } pid_array_t; | |
| static int | |
| pid_array_init(pid_array_t *arr) | |
| { | |
| arr->capacity = 64; | |
| arr->count = 0; | |
| arr->pids = (pid_t *)PyMem_Malloc(arr->capacity * sizeof(pid_t)); | |
| if (arr->pids == NULL) { | |
| PyErr_NoMemory(); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static void | |
| pid_array_cleanup(pid_array_t *arr) | |
| { | |
| if (arr->pids != NULL) { | |
| PyMem_Free(arr->pids); | |
| arr->pids = NULL; | |
| } | |
| arr->count = 0; | |
| arr->capacity = 0; | |
| } | |
| static int | |
| pid_array_append(pid_array_t *arr, pid_t pid) | |
| { | |
| if (arr->count >= arr->capacity) { | |
| /* Check for overflow before multiplication */ | |
| if (arr->capacity > SIZE_MAX / 2) { | |
| PyErr_SetString(PyExc_OverflowError, "PID array capacity overflow"); | |
| return -1; | |
| } | |
| size_t new_capacity = arr->capacity * 2; | |
| /* Check allocation size won't overflow */ | |
| if (new_capacity > SIZE_MAX / sizeof(pid_t)) { | |
| PyErr_SetString(PyExc_OverflowError, "PID array size overflow"); | |
| return -1; | |
| } | |
| pid_t *new_pids = (pid_t *)PyMem_Realloc(arr->pids, new_capacity * sizeof(pid_t)); | |
| if (new_pids == NULL) { | |
| PyErr_NoMemory(); | |
| return -1; | |
| } | |
| arr->pids = new_pids; | |
| arr->capacity = new_capacity; | |
| } | |
| arr->pids[arr->count++] = pid; | |
| return 0; | |
| } | |
| static int | |
| pid_array_contains(pid_array_t *arr, pid_t pid) | |
| { | |
| for (size_t i = 0; i < arr->count; i++) { | |
| if (arr->pids[i] == pid) { | |
| return 1; | |
| } | |
| } | |
| return 0; | |
| } | |
| /* ============================================================================ | |
| * SHARED BFS HELPER | |
| * ============================================================================ */ | |
| /* Find child PIDs using BFS traversal of the pid->ppid mapping. | |
| * all_pids and ppids must have the same count (parallel arrays). | |
| * Returns 0 on success, -1 on error. */ | |
| UNUSED static int | |
| find_children_bfs(pid_t target_pid, int recursive, | |
| pid_t *all_pids, pid_t *ppids, size_t pid_count, | |
| pid_array_t *result) | |
| { | |
| int retval = -1; | |
| pid_array_t to_process = {0}; | |
| if (pid_array_init(&to_process) < 0) { | |
| goto done; | |
| } | |
| if (pid_array_append(&to_process, target_pid) < 0) { | |
| goto done; | |
| } | |
| size_t process_idx = 0; | |
| while (process_idx < to_process.count) { | |
| pid_t current_pid = to_process.pids[process_idx++]; | |
| for (size_t i = 0; i < pid_count; i++) { | |
| if (ppids[i] != current_pid) { | |
| continue; | |
| } | |
| pid_t child_pid = all_pids[i]; | |
| if (pid_array_contains(result, child_pid)) { | |
| continue; | |
| } | |
| if (pid_array_append(result, child_pid) < 0) { | |
| goto done; | |
| } | |
| if (recursive && pid_array_append(&to_process, child_pid) < 0) { | |
| goto done; | |
| } | |
| } | |
| if (!recursive) { | |
| break; | |
| } | |
| } | |
| retval = 0; | |
| done: | |
| pid_array_cleanup(&to_process); | |
| return retval; | |
| } | |
| /* ============================================================================ | |
| * LINUX IMPLEMENTATION | |
| * ============================================================================ */ | |
| #if defined(__linux__) | |
| /* Parse /proc/{pid}/stat to get parent PID */ | |
| static pid_t | |
| get_ppid_linux(pid_t pid) | |
| { | |
| char stat_path[64]; | |
| char buffer[2048]; | |
| snprintf(stat_path, sizeof(stat_path), "/proc/%d/stat", (int)pid); | |
| int fd = open(stat_path, O_RDONLY); | |
| if (fd == -1) { | |
| return -1; | |
| } | |
| ssize_t n = read(fd, buffer, sizeof(buffer) - 1); | |
| close(fd); | |
| if (n <= 0) { | |
| return -1; | |
| } | |
| buffer[n] = '\0'; | |
| /* Find closing paren of comm field - stat format: pid (comm) state ppid ... */ | |
| char *p = strrchr(buffer, ')'); | |
| if (!p) { | |
| return -1; | |
| } | |
| /* Skip ") " with bounds checking */ | |
| char *end = buffer + n; | |
| p += 2; | |
| if (p >= end) { | |
| return -1; | |
| } | |
| if (*p == ' ') { | |
| p++; | |
| if (p >= end) { | |
| return -1; | |
| } | |
| } | |
| /* Parse: state ppid */ | |
| char state; | |
| int ppid; | |
| if (sscanf(p, "%c %d", &state, &ppid) != 2) { | |
| return -1; | |
| } | |
| return (pid_t)ppid; | |
| } | |
| static int | |
| get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) | |
| { | |
| int retval = -1; | |
| pid_array_t all_pids = {0}; | |
| pid_array_t ppids = {0}; | |
| DIR *proc_dir = NULL; | |
| if (pid_array_init(&all_pids) < 0) { | |
| goto done; | |
| } | |
| if (pid_array_init(&ppids) < 0) { | |
| goto done; | |
| } | |
| proc_dir = opendir("/proc"); | |
| if (!proc_dir) { | |
| PyErr_SetFromErrnoWithFilename(PyExc_OSError, "/proc"); | |
| goto done; | |
| } | |
| /* Single pass: collect PIDs and their PPIDs together */ | |
| for (;;) { | |
| errno = 0; | |
| struct dirent *entry = readdir(proc_dir); | |
| if (entry == NULL) { | |
| if (errno != 0) { | |
| int err = errno; | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno_with_filename(err, "/proc", | |
| "Failed to read process directory '/proc': %S", | |
| message); | |
| Py_DECREF(message); | |
| } | |
| goto done; | |
| } | |
| break; | |
| } | |
| /* Skip non-numeric entries (also skips . and ..) */ | |
| if (entry->d_name[0] < '1' || entry->d_name[0] > '9') { | |
| continue; | |
| } | |
| char *endptr; | |
| long pid_long = strtol(entry->d_name, &endptr, 10); | |
| if (*endptr != '\0' || pid_long <= 0) { | |
| continue; | |
| } | |
| pid_t pid = (pid_t)pid_long; | |
| pid_t ppid = get_ppid_linux(pid); | |
| if (ppid < 0) { | |
| continue; | |
| } | |
| if (pid_array_append(&all_pids, pid) < 0 || | |
| pid_array_append(&ppids, ppid) < 0) { | |
| goto done; | |
| } | |
| } | |
| if (closedir(proc_dir) != 0) { | |
| int err = errno; | |
| proc_dir = NULL; | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno_with_filename(err, "/proc", | |
| "Failed to close process directory '/proc': %S", | |
| message); | |
| Py_DECREF(message); | |
| } | |
| goto done; | |
| } | |
| proc_dir = NULL; | |
| if (find_children_bfs(target_pid, recursive, | |
| all_pids.pids, ppids.pids, all_pids.count, | |
| result) < 0) { | |
| goto done; | |
| } | |
| retval = 0; | |
| done: | |
| if (proc_dir) { | |
| closedir(proc_dir); | |
| } | |
| pid_array_cleanup(&all_pids); | |
| pid_array_cleanup(&ppids); | |
| return retval; | |
| } | |
| #endif /* __linux__ */ | |
| /* ============================================================================ | |
| * MACOS IMPLEMENTATION | |
| * ============================================================================ */ | |
| #if defined(__APPLE__) && TARGET_OS_OSX | |
| #include <libproc.h> | |
| #include <sys/proc_info.h> | |
| static int | |
| get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) | |
| { | |
| int retval = -1; | |
| pid_t *pid_list = NULL; | |
| pid_t *ppids = NULL; | |
| /* Get count of all PIDs */ | |
| int n_pids = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0); | |
| if (n_pids <= 0) { | |
| PyErr_SetString(PyExc_OSError, "Failed to get process count"); | |
| goto done; | |
| } | |
| n_pids /= sizeof(pid_t); | |
| /* Allocate buffer for PIDs (add some slack for new processes) */ | |
| int buffer_size = n_pids + 64; | |
| pid_list = (pid_t *)PyMem_Malloc(buffer_size * sizeof(pid_t)); | |
| if (!pid_list) { | |
| PyErr_NoMemory(); | |
| goto done; | |
| } | |
| /* Get actual PIDs */ | |
| int actual = proc_listpids(PROC_ALL_PIDS, 0, pid_list, buffer_size * sizeof(pid_t)); | |
| if (actual <= 0) { | |
| PyErr_SetString(PyExc_OSError, "Failed to list PIDs"); | |
| goto done; | |
| } | |
| actual /= sizeof(pid_t); | |
| /* Build pid -> ppid mapping */ | |
| ppids = (pid_t *)PyMem_Malloc(actual * sizeof(pid_t)); | |
| if (!ppids) { | |
| PyErr_NoMemory(); | |
| goto done; | |
| } | |
| /* Get parent PIDs for each process */ | |
| int valid_count = 0; | |
| for (int i = 0; i < actual; i++) { | |
| struct proc_bsdinfo proc_info; | |
| int ret = proc_pidinfo(pid_list[i], PROC_PIDTBSDINFO, 0, | |
| &proc_info, sizeof(proc_info)); | |
| if (ret != sizeof(proc_info)) { | |
| continue; | |
| } | |
| pid_list[valid_count] = pid_list[i]; | |
| ppids[valid_count] = proc_info.pbi_ppid; | |
| valid_count++; | |
| } | |
| if (find_children_bfs(target_pid, recursive, | |
| pid_list, ppids, valid_count, | |
| result) < 0) { | |
| goto done; | |
| } | |
| retval = 0; | |
| done: | |
| PyMem_Free(pid_list); | |
| PyMem_Free(ppids); | |
| return retval; | |
| } | |
| #endif /* __APPLE__ && TARGET_OS_OSX */ | |
| /* ============================================================================ | |
| * WINDOWS IMPLEMENTATION | |
| * ============================================================================ */ | |
| #ifdef MS_WINDOWS | |
| static int | |
| get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) | |
| { | |
| int retval = -1; | |
| pid_array_t all_pids = {0}; | |
| pid_array_t ppids = {0}; | |
| HANDLE snapshot = INVALID_HANDLE_VALUE; | |
| snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); | |
| if (snapshot == INVALID_HANDLE_VALUE) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| goto done; | |
| } | |
| if (pid_array_init(&all_pids) < 0) { | |
| goto done; | |
| } | |
| if (pid_array_init(&ppids) < 0) { | |
| goto done; | |
| } | |
| /* Single pass: collect PIDs and PPIDs together */ | |
| PROCESSENTRY32 pe; | |
| pe.dwSize = sizeof(PROCESSENTRY32); | |
| if (!Process32First(snapshot, &pe)) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| goto done; | |
| } | |
| do { | |
| if (pid_array_append(&all_pids, (pid_t)pe.th32ProcessID) < 0 || | |
| pid_array_append(&ppids, (pid_t)pe.th32ParentProcessID) < 0) { | |
| goto done; | |
| } | |
| } while (Process32Next(snapshot, &pe)); | |
| DWORD error = GetLastError(); | |
| if (error != ERROR_NO_MORE_FILES) { | |
| PyErr_SetFromWindowsErr(error); | |
| goto done; | |
| } | |
| CloseHandle(snapshot); | |
| snapshot = INVALID_HANDLE_VALUE; | |
| if (find_children_bfs(target_pid, recursive, | |
| all_pids.pids, ppids.pids, all_pids.count, | |
| result) < 0) { | |
| goto done; | |
| } | |
| retval = 0; | |
| done: | |
| if (snapshot != INVALID_HANDLE_VALUE) { | |
| CloseHandle(snapshot); | |
| } | |
| pid_array_cleanup(&all_pids); | |
| pid_array_cleanup(&ppids); | |
| return retval; | |
| } | |
| #endif /* MS_WINDOWS */ | |
| /* ============================================================================ | |
| * UNSUPPORTED PLATFORM STUB | |
| * ============================================================================ */ | |
| #if !defined(__linux__) && !(defined(__APPLE__) && TARGET_OS_OSX) && !defined(MS_WINDOWS) | |
| static int | |
| get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) | |
| { | |
| PyErr_SetString(PyExc_NotImplementedError, | |
| "Subprocess enumeration not supported on this platform"); | |
| return -1; | |
| } | |
| #endif | |
| /* ============================================================================ | |
| * PUBLIC API | |
| * ============================================================================ */ | |
| PyObject * | |
| enumerate_child_pids(pid_t target_pid, int recursive) | |
| { | |
| pid_array_t result; | |
| if (pid_array_init(&result) < 0) { | |
| return NULL; | |
| } | |
| if (get_child_pids_platform(target_pid, recursive, &result) < 0) { | |
| pid_array_cleanup(&result); | |
| return NULL; | |
| } | |
| /* Convert to Python list */ | |
| PyObject *list = PyList_New(result.count); | |
| if (list == NULL) { | |
| pid_array_cleanup(&result); | |
| return NULL; | |
| } | |
| for (size_t i = 0; i < result.count; i++) { | |
| PyObject *pid_obj = PyLong_FromLong((long)result.pids[i]); | |
| if (pid_obj == NULL) { | |
| Py_DECREF(list); | |
| pid_array_cleanup(&result); | |
| return NULL; | |
| } | |
| PyList_SET_ITEM(list, i, pid_obj); | |
| } | |
| pid_array_cleanup(&result); | |
| return list; | |
| } |