gdb/Python: Added ThreadExitedEvent

v6:
Fix comments.
Fix copyright
Remove unnecessary test suite stuff. save_var had to stay, as it mutates
some test suite state that otherwise fails.

v5:
Did what Tom Tromey requested in v4; which can be found here: https://pi.simark.ca/gdb-patches/87pmjm0xar.fsf@tromey.com/

v4:
Doc formatting fixed.

v3:
Eli:
Updated docs & NEWS to reflect new changes. Added
a reference from the .ptid attribute of the ThreadExitedEvent
to the ptid attribute of InferiorThread. To do this,
I've added an anchor to that attribute.

Tom:
Tom requested that I should probably just emit the thread object;
I ran into two issues for this, which I could not resolve in this patch;

1 - The Thread Object (the python type) checks it's own validity
by doing a comparison of it's `thread_info* thread` to nullptr. This
means that any access of it's attributes may (probably, since we are
in "async" land) throw Python exceptions because the thread has been
removed from the thread object. Therefore I've decided in v3 of this
patch to just emit most of the same fields that gdb.InferiorThread has, namely
global_num, name, num and ptid (the 3-attribute tuple provided by
gdb.InferiorThread.ptid).

2 - A python user can hold a global reference to an exiting thread. Thus
in order to have a ThreadExit event that can provide attribute access
reliably (both as a global reference, but also inside the thread exit
handler, as we can never guarantee that it's executed _before_ the
thread_info pointer is removed from the gdbpy thread object),
the `thread_info *` thread pointer must not be null. However, this
comes at the cost of gdb.InferiorThread believing it is "valid" - which means,
that if a user holds takes a global reference to that
exiting event thread object, they can some time later do `t.switch()` at which
point GDB will 'explode' so to speak.

v2:
Fixed white space issues and NULL/nullptr stuff,
as requested by Tom Tromey.

v1:
Currently no event is emitted for a thread exit.

This adds this functionality by emitting a new gdb.ThreadExitedEvent.

It currently provides four attributes:
- global_num: The GDB assigned global thread number
- num: the per-inferior thread number
- name: name of the thread or none if not set
- ptid: the PTID of the thread, a 3-attribute tuple, identical to
InferiorThread.ptid attribute

Added info to docs & the NEWS file as well.

Added test to test suite.

Fixed formatting.

Feedback wanted and appreciated.
This commit is contained in:
Simon Farre 2022-04-20 10:25:47 +02:00
parent f1a614dc8f
commit 28ab59607e
10 changed files with 172 additions and 0 deletions

View File

@ -140,6 +140,8 @@ info main
* Python API
** gdb.ThreadExitedEvent added. Emits a ThreadEvent.
** The gdb.unwinder.Unwinder.name attribute is now read-only.
** The name argument passed to gdb.unwinder.Unwinder.__init__ must

View File

@ -3835,6 +3835,15 @@ This has a single attribute:
The new thread.
@end defvar
@item events.thread_exited
This is emitted when @value{GDBN} notices a thread has exited. The event
is of type @code{gdb.ThreadExitedEvent} which extends @code{gdb.ThreadEvent}.
This has a single attribute:
@defvar ThreadExitedEvent.inferior_thread
The exiting thread.
@end defvar
@item events.gdb_exiting
This is emitted when @value{GDBN} exits. This event is not emitted if
@value{GDBN} exits as a result of an internal error, or after an
@ -3899,6 +3908,7 @@ make Python breakpoints thread-specific, for example
(@pxref{python_breakpoint_thread,,The Breakpoint.thread attribute}).
@end defvar
@anchor{inferior_thread_ptid}
@defvar InferiorThread.ptid
ID of the thread, as assigned by the operating system. This attribute is a
tuple containing three integers. The first is the Process ID (PID); the second

View File

@ -32,6 +32,7 @@ GDB_PY_DEFINE_EVENT(clear_objfiles)
GDB_PY_DEFINE_EVENT(new_inferior)
GDB_PY_DEFINE_EVENT(inferior_deleted)
GDB_PY_DEFINE_EVENT(new_thread)
GDB_PY_DEFINE_EVENT(thread_exited)
GDB_PY_DEFINE_EVENT(inferior_call)
GDB_PY_DEFINE_EVENT(memory_changed)
GDB_PY_DEFINE_EVENT(register_changed)

View File

@ -51,6 +51,11 @@ GDB_PY_DEFINE_EVENT_TYPE (new_thread,
"GDB new thread event object",
thread_event_object_type);
GDB_PY_DEFINE_EVENT_TYPE (thread_exited,
"ThreadExitedEvent",
"GDB thread exited event object",
event_object_type);
GDB_PY_DEFINE_EVENT_TYPE (new_inferior,
"NewInferiorEvent",
"GDB new inferior event object",

View File

@ -61,6 +61,9 @@ extern int emit_memory_changed_event (CORE_ADDR addr, ssize_t len);
extern int evpy_emit_event (PyObject *event,
eventregistry_object *registry);
/* Emits a thread exit event for THREAD */
extern int emit_thread_exit_event (thread_info * thread);
extern gdbpy_ref<> create_event_object (PyTypeObject *py_type);
/* thread events can either be thread specific or process wide. If gdb is

View File

@ -371,6 +371,9 @@ delete_thread_object (struct thread_info *tp, int ignore)
if (inf_obj == NULL)
return;
if (emit_thread_exit_event (tp) < 0)
gdbpy_print_stack ();
auto it = inf_obj->threads->find (tp);
if (it != inf_obj->threads->end ())
{

View File

@ -53,3 +53,24 @@ create_thread_event_object (PyTypeObject *py_type, PyObject *thread)
return thread_event_obj;
}
/* Emits a thread exit event for THREAD */
int
emit_thread_exit_event (thread_info * thread)
{
if (evregpy_no_listeners_p (gdb_py_events.thread_exited))
return 0;
auto py_thr = thread_to_thread_object (thread);
if (py_thr == nullptr)
return -1;
auto inf_thr = create_thread_event_object (&thread_exited_event_object_type,
py_thr.get ());
if (inf_thr == nullptr)
return -1;
return evpy_emit_event (inf_thr.get (), gdb_py_events.thread_exited);
}

View File

@ -0,0 +1,37 @@
/* This testcase is part of GDB, the GNU debugger.
Copyright 2022-2023 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
pthread_t thread2_id;
pthread_t thread3_id;
void* do_thread (void* d)
{
return NULL;
}
int main (void)
{
pthread_create (&thread2_id, NULL, do_thread, NULL);
pthread_join (thread2_id, NULL);
pthread_create (&thread3_id, NULL, do_thread, NULL);
pthread_join (thread3_id, NULL);
return 12;
}

View File

@ -0,0 +1,44 @@
# Copyright (C) 2022-202 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
load_lib gdb-python.exp
standard_testfile
if {[gdb_compile_pthreads "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug}] != "" } {
return -1
}
save_vars { GDBFLAGS } {
clean_restart $testfile
}
set pyfile [gdb_remote_download host ${srcdir}/${subdir}/py-thread-exited.py]
gdb_test_no_output "source ${pyfile}" "load python file"
gdb_test "test-events" "Event testers registered."
if ![runto_main] {
return -1
}
gdb_breakpoint 37 "last of main"
gdb_continue_to_breakpoint "continue to breakpoint"
gdb_test "python print(threadOneExit)" \
".*event type: thread-exited. global num: 2.*"
gdb_test "python print(threadTwoExit)" \
".*event type: thread-exited. global num: 3.*"

View File

@ -0,0 +1,46 @@
# Copyright 2022-2023 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import gdb
threadOneExit = ""
threadTwoExit = ""
# we don't want to overwrite the 2nd thread's exit event, thus
# store it here. we don't care about it though.
mainThreadExit = ""
def thread_exited_handler(event):
global threadOneExit, threadTwoExit, mainThreadExit
print("{}".format(event))
assert isinstance(event, gdb.ThreadExitedEvent)
if threadOneExit == "":
threadOneExit = "event type: thread-exited. global num: {}".format(event.inferior_thread.global_num)
else:
if threadTwoExit == "":
threadTwoExit = "event type: thread-exited. global num: {}".format(event.inferior_thread.global_num)
else:
mainThreadExit = "event type: thread-exited. global num: {}".format(event.inferior_thread.global_num)
class test_events(gdb.Command):
"""Test events."""
def __init__(self):
gdb.Command.__init__(self, "test-events", gdb.COMMAND_STACK)
def invoke(self, arg, from_tty):
gdb.events.thread_exited.connect(thread_exited_handler)
print("Event testers registered.")
test_events()