Skip to content

Commit 7dbf221

Browse files
committed
Add get_child_pids function to _remote_debugging module
This implements platform-specific child process enumeration for use by the profiler. On Linux it parses /proc/{pid}/stat to build a parent- child map and then walks the tree from the target PID. On macOS it uses proc_listchildpids() when available, falling back to scanning all processes with proc_pidinfo(). On Windows it uses CreateToolhelp32Snapshot with TH32CS_SNAPPROCESS to iterate through all processes. The function returns a Python list of PIDs representing all descendants of the given process. The recursive parameter controls whether only direct children or all descendants are returned. This is the building block needed for the --children flag in the sampling profiler CLI.
1 parent 3e3c4e5 commit 7dbf221

File tree

6 files changed

+686
-3
lines changed

6 files changed

+686
-3
lines changed

Modules/Setup.stdlib.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
@MODULE__PICKLE_TRUE@_pickle _pickle.c
4242
@MODULE__QUEUE_TRUE@_queue _queuemodule.c
4343
@MODULE__RANDOM_TRUE@_random _randommodule.c
44-
@MODULE__REMOTE_DEBUGGING_TRUE@_remote_debugging _remote_debugging/module.c _remote_debugging/object_reading.c _remote_debugging/code_objects.c _remote_debugging/frames.c _remote_debugging/frame_cache.c _remote_debugging/threads.c _remote_debugging/asyncio.c
44+
@MODULE__REMOTE_DEBUGGING_TRUE@_remote_debugging _remote_debugging/module.c _remote_debugging/object_reading.c _remote_debugging/code_objects.c _remote_debugging/frames.c _remote_debugging/frame_cache.c _remote_debugging/threads.c _remote_debugging/asyncio.c _remote_debugging/subprocess.c
4545
@MODULE__STRUCT_TRUE@_struct _struct.c
4646

4747
# build supports subinterpreters

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,16 @@ extern int process_thread_for_async_stack_trace(
589589
void *context
590590
);
591591

592+
/* ============================================================================
593+
* SUBPROCESS ENUMERATION FUNCTION DECLARATIONS
594+
* ============================================================================ */
595+
596+
/* Get all child PIDs of a process.
597+
* Returns a new Python list of PIDs, or NULL on error with exception set.
598+
* If recursive is true, includes all descendants (children, grandchildren, etc.)
599+
*/
600+
extern PyObject *enumerate_child_pids(pid_t target_pid, int recursive);
601+
592602
#ifdef __cplusplus
593603
}
594604
#endif

Modules/_remote_debugging/module.c

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
350350
}
351351

352352
// Validate that the debug offsets are valid
353-
if(validate_debug_offsets(&self->debug_offsets) == -1) {
353+
if (validate_debug_offsets(&self->debug_offsets) == -1) {
354354
set_exception_cause(self, PyExc_RuntimeError, "Invalid debug offsets found");
355355
return -1;
356356
}
@@ -933,7 +933,7 @@ RemoteUnwinder_dealloc(PyObject *op)
933933
_Py_hashtable_destroy(self->code_object_cache);
934934
}
935935
#ifdef MS_WINDOWS
936-
if(self->win_process_buffer != NULL) {
936+
if (self->win_process_buffer != NULL) {
937937
PyMem_Free(self->win_process_buffer);
938938
}
939939
#endif
@@ -1122,7 +1122,129 @@ static PyModuleDef_Slot remote_debugging_slots[] = {
11221122
{0, NULL},
11231123
};
11241124

1125+
/* ============================================================================
1126+
* MODULE-LEVEL FUNCTIONS
1127+
* ============================================================================ */
1128+
1129+
/*[clinic input]
1130+
_remote_debugging.get_child_pids
1131+
1132+
pid: int
1133+
Process ID of the parent process
1134+
*
1135+
recursive: bool = True
1136+
If True, return all descendants (children, grandchildren, etc.).
1137+
If False, return only direct children.
1138+
1139+
Get all child process IDs of the given process.
1140+
1141+
Returns a list of child process IDs. Returns an empty list if no children
1142+
are found.
1143+
1144+
This function provides a snapshot of child processes at a moment in time.
1145+
Child processes may exit or new ones may be created after the list is returned.
1146+
1147+
Raises:
1148+
OSError: If unable to enumerate processes
1149+
NotImplementedError: If not supported on this platform
1150+
[clinic start generated code]*/
1151+
1152+
static PyObject *
1153+
_remote_debugging_get_child_pids_impl(PyObject *module, int pid, int recursive);
1154+
1155+
static PyObject *
1156+
_remote_debugging_get_child_pids(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
1157+
{
1158+
PyObject *return_value = NULL;
1159+
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
1160+
1161+
#define NUM_KEYWORDS 2
1162+
static struct {
1163+
PyGC_Head _this_is_not_used;
1164+
PyObject_VAR_HEAD
1165+
Py_hash_t ob_hash;
1166+
PyObject *ob_item[NUM_KEYWORDS];
1167+
} _kwtuple = {
1168+
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
1169+
.ob_hash = -1,
1170+
.ob_item = { &_Py_ID(pid), &_Py_ID(recursive), },
1171+
};
1172+
#undef NUM_KEYWORDS
1173+
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
1174+
1175+
#else // !Py_BUILD_CORE
1176+
# define KWTUPLE NULL
1177+
#endif // !Py_BUILD_CORE
1178+
1179+
static const char * const _keywords[] = {"pid", "recursive", NULL};
1180+
static _PyArg_Parser _parser = {
1181+
.keywords = _keywords,
1182+
.fname = "get_child_pids",
1183+
.kwtuple = KWTUPLE,
1184+
};
1185+
#undef KWTUPLE
1186+
PyObject *argsbuf[2];
1187+
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
1188+
int pid;
1189+
int recursive = 1;
1190+
1191+
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
1192+
/*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
1193+
if (!args) {
1194+
goto exit;
1195+
}
1196+
pid = PyLong_AsInt(args[0]);
1197+
if (pid == -1 && PyErr_Occurred()) {
1198+
goto exit;
1199+
}
1200+
if (!noptargs) {
1201+
goto skip_optional_kwonly;
1202+
}
1203+
recursive = PyObject_IsTrue(args[1]);
1204+
if (recursive < 0) {
1205+
goto exit;
1206+
}
1207+
skip_optional_kwonly:
1208+
return_value = _remote_debugging_get_child_pids_impl(module, pid, recursive);
1209+
1210+
exit:
1211+
return return_value;
1212+
}
1213+
1214+
PyDoc_STRVAR(_remote_debugging_get_child_pids__doc__,
1215+
"get_child_pids($module, /, pid, *, recursive=True)\n"
1216+
"--\n"
1217+
"\n"
1218+
"Get all child process IDs of the given process.\n"
1219+
"\n"
1220+
" pid\n"
1221+
" Process ID of the parent process\n"
1222+
" recursive\n"
1223+
" If True, return all descendants (children, grandchildren, etc.).\n"
1224+
" If False, return only direct children.\n"
1225+
"\n"
1226+
"Returns a list of child process IDs. Returns an empty list if no children\n"
1227+
"are found.\n"
1228+
"\n"
1229+
"This function provides a snapshot of child processes at a moment in time.\n"
1230+
"Child processes may exit or new ones may be created after the list is returned.\n"
1231+
"\n"
1232+
"Raises:\n"
1233+
" OSError: If unable to enumerate processes\n"
1234+
" NotImplementedError: If not supported on this platform");
1235+
1236+
#define _REMOTE_DEBUGGING_GET_CHILD_PIDS_METHODDEF \
1237+
{"get_child_pids", _PyCFunction_CAST(_remote_debugging_get_child_pids), METH_FASTCALL|METH_KEYWORDS, _remote_debugging_get_child_pids__doc__},
1238+
/*[clinic end generated code: output=b21aaa012edb5379 input=c445e924c6be29f2]*/
1239+
1240+
static PyObject *
1241+
_remote_debugging_get_child_pids_impl(PyObject *module, int pid, int recursive)
1242+
{
1243+
return enumerate_child_pids((pid_t)pid, recursive);
1244+
}
1245+
11251246
static PyMethodDef remote_debugging_methods[] = {
1247+
_REMOTE_DEBUGGING_GET_CHILD_PIDS_METHODDEF
11261248
{NULL, NULL, 0, NULL},
11271249
};
11281250

0 commit comments

Comments
 (0)