]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-59705: Add _thread.set_name() function (#127338)
authorVictor Stinner <vstinner@python.org>
Fri, 6 Dec 2024 16:27:12 +0000 (17:27 +0100)
committerGitHub <noreply@github.com>
Fri, 6 Dec 2024 16:27:12 +0000 (16:27 +0000)
On Linux, threading.Thread now sets the thread name to the operating
system.

* configure now checks if pthread_getname_np()
  and pthread_setname_np() functions are available.
* Add PYTHREAD_NAME_MAXLEN macro.
* Add _thread._NAME_MAXLEN constant for test_threading.

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Lib/test/test_threading.py
Lib/threading.py
Misc/NEWS.d/next/Library/2024-11-27-17-04-38.gh-issue-59705.sAGyvs.rst [new file with mode: 0644]
Modules/_threadmodule.c
Modules/clinic/_threadmodule.c.h [new file with mode: 0644]
configure
configure.ac
pyconfig.h.in

index fe225558fc4f0b0fcb8938a0a3dc2650dc0f501a..d05161f46f10348bbc9a156c2e70653fefee1358 100644 (file)
@@ -2104,6 +2104,66 @@ class MiscTestCase(unittest.TestCase):
         support.check__all__(self, threading, ('threading', '_thread'),
                              extra=extra, not_exported=not_exported)
 
+    @unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name")
+    @unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name")
+    def test_set_name(self):
+        # set_name() limit in bytes
+        truncate = getattr(_thread, "_NAME_MAXLEN", None)
+        limit = truncate or 100
+
+        tests = [
+            # test short ASCII name
+            "CustomName",
+
+            # test short non-ASCII name
+            "namé€",
+
+            # embedded null character: name is truncated
+            # at the first null character
+            "embed\0null",
+
+            # Test long ASCII names (not truncated)
+            "x" * limit,
+
+            # Test long ASCII names (truncated)
+            "x" * (limit + 10),
+
+            # Test long non-ASCII name (truncated)
+            "x" * (limit - 1) + "é€",
+        ]
+        if os_helper.FS_NONASCII:
+            tests.append(f"nonascii:{os_helper.FS_NONASCII}")
+        if os_helper.TESTFN_UNENCODABLE:
+            tests.append(os_helper.TESTFN_UNENCODABLE)
+
+        if sys.platform.startswith("solaris"):
+            encoding = "utf-8"
+        else:
+            encoding = sys.getfilesystemencoding()
+
+        def work():
+            nonlocal work_name
+            work_name = _thread._get_name()
+
+        for name in tests:
+            encoded = name.encode(encoding, "replace")
+            if b'\0' in encoded:
+                encoded = encoded.split(b'\0', 1)[0]
+            if truncate is not None:
+                encoded = encoded[:truncate]
+            if sys.platform.startswith("solaris"):
+                expected = encoded.decode("utf-8", "surrogateescape")
+            else:
+                expected = os.fsdecode(encoded)
+
+            with self.subTest(name=name, expected=expected):
+                work_name = None
+                thread = threading.Thread(target=work, name=name)
+                thread.start()
+                thread.join()
+                self.assertEqual(work_name, expected,
+                                 f"{len(work_name)=} and {len(expected)=}")
+
 
 class InterruptMainTests(unittest.TestCase):
     def check_interrupt_main_with_signal_handler(self, signum):
index 94ea2f081783696ed03928a085c27bee6a46cdca..3abd22a2aa1b7214d02ddca95687067be0229250 100644 (file)
@@ -48,6 +48,10 @@ try:
     __all__.append('get_native_id')
 except AttributeError:
     _HAVE_THREAD_NATIVE_ID = False
+try:
+    _set_name = _thread.set_name
+except AttributeError:
+    _set_name = None
 ThreadError = _thread.error
 try:
     _CRLock = _thread.RLock
@@ -1027,6 +1031,11 @@ class Thread:
             self._set_ident()
             if _HAVE_THREAD_NATIVE_ID:
                 self._set_native_id()
+            if _set_name is not None and self._name:
+                try:
+                    _set_name(self._name)
+                except OSError:
+                    pass
             self._started.set()
             with _active_limbo_lock:
                 _active[self._ident] = self
diff --git a/Misc/NEWS.d/next/Library/2024-11-27-17-04-38.gh-issue-59705.sAGyvs.rst b/Misc/NEWS.d/next/Library/2024-11-27-17-04-38.gh-issue-59705.sAGyvs.rst
new file mode 100644 (file)
index 0000000..a8c7b3d
--- /dev/null
@@ -0,0 +1,2 @@
+On Linux, :class:`threading.Thread` now sets the thread name to the
+operating system. Patch by Victor Stinner.
index 4a45445e2f62db5b23886ef518373f33f7359e0d..35c032fbeaa94f78bf8c1dc6bf5891749a8e4dbd 100644 (file)
@@ -17,6 +17,8 @@
 #  include <signal.h>             // SIGINT
 #endif
 
+#include "clinic/_threadmodule.c.h"
+
 // ThreadError is just an alias to PyExc_RuntimeError
 #define ThreadError PyExc_RuntimeError
 
@@ -44,6 +46,13 @@ get_thread_state(PyObject *module)
     return (thread_module_state *)state;
 }
 
+
+/*[clinic input]
+module _thread
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/
+
+
 // _ThreadHandle type
 
 // Handles state transitions according to the following diagram:
@@ -2354,6 +2363,96 @@ PyDoc_STRVAR(thread__get_main_thread_ident_doc,
 Internal only. Return a non-zero integer that uniquely identifies the main thread\n\
 of the main interpreter.");
 
+
+#ifdef HAVE_PTHREAD_GETNAME_NP
+/*[clinic input]
+_thread._get_name
+
+Get the name of the current thread.
+[clinic start generated code]*/
+
+static PyObject *
+_thread__get_name_impl(PyObject *module)
+/*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/
+{
+    // Linux and macOS are limited to respectively 16 and 64 bytes
+    char name[100];
+    pthread_t thread = pthread_self();
+    int rc = pthread_getname_np(thread, name, Py_ARRAY_LENGTH(name));
+    if (rc) {
+        errno = rc;
+        return PyErr_SetFromErrno(PyExc_OSError);
+    }
+
+#ifdef __sun
+    return PyUnicode_DecodeUTF8(name, strlen(name), "surrogateescape");
+#else
+    return PyUnicode_DecodeFSDefault(name);
+#endif
+}
+#endif  // HAVE_PTHREAD_GETNAME_NP
+
+
+#ifdef HAVE_PTHREAD_SETNAME_NP
+/*[clinic input]
+_thread.set_name
+
+    name as name_obj: unicode
+
+Set the name of the current thread.
+[clinic start generated code]*/
+
+static PyObject *
+_thread_set_name_impl(PyObject *module, PyObject *name_obj)
+/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/
+{
+#ifdef __sun
+    // Solaris always uses UTF-8
+    const char *encoding = "utf-8";
+#else
+    // Encode the thread name to the filesystem encoding using the "replace"
+    // error handler
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    const char *encoding = interp->unicode.fs_codec.encoding;
+#endif
+    PyObject *name_encoded;
+    name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace");
+    if (name_encoded == NULL) {
+        return NULL;
+    }
+
+#ifdef PYTHREAD_NAME_MAXLEN
+    // Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed
+    size_t len = PyBytes_GET_SIZE(name_encoded);
+    if (len > PYTHREAD_NAME_MAXLEN) {
+        PyObject *truncated;
+        truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded),
+                                              PYTHREAD_NAME_MAXLEN);
+        if (truncated == NULL) {
+            Py_DECREF(name_encoded);
+            return NULL;
+        }
+        Py_SETREF(name_encoded, truncated);
+    }
+#endif
+
+    const char *name = PyBytes_AS_STRING(name_encoded);
+#ifdef __APPLE__
+    int rc = pthread_setname_np(name);
+#else
+    pthread_t thread = pthread_self();
+    int rc = pthread_setname_np(thread, name);
+#endif
+    Py_DECREF(name_encoded);
+    if (rc) {
+        errno = rc;
+        return PyErr_SetFromErrno(PyExc_OSError);
+    }
+    Py_RETURN_NONE;
+}
+#endif  // HAVE_PTHREAD_SETNAME_NP
+
+
 static PyMethodDef thread_methods[] = {
     {"start_new_thread",        (PyCFunction)thread_PyThread_start_new_thread,
      METH_VARARGS, start_new_thread_doc},
@@ -2393,6 +2492,8 @@ static PyMethodDef thread_methods[] = {
      METH_O, thread__make_thread_handle_doc},
     {"_get_main_thread_ident", thread__get_main_thread_ident,
      METH_NOARGS, thread__get_main_thread_ident_doc},
+    _THREAD_SET_NAME_METHODDEF
+    _THREAD__GET_NAME_METHODDEF
     {NULL,                      NULL}           /* sentinel */
 };
 
@@ -2484,6 +2585,13 @@ thread_module_exec(PyObject *module)
 
     llist_init(&state->shutdown_handles);
 
+#ifdef PYTHREAD_NAME_MAXLEN
+    if (PyModule_AddIntConstant(module, "_NAME_MAXLEN",
+                                PYTHREAD_NAME_MAXLEN) < 0) {
+        return -1;
+    }
+#endif
+
     return 0;
 }
 
diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h
new file mode 100644 (file)
index 0000000..8f0507d
--- /dev/null
@@ -0,0 +1,104 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+#  include "pycore_gc.h"          // PyGC_Head
+#  include "pycore_runtime.h"     // _Py_ID()
+#endif
+#include "pycore_modsupport.h"    // _PyArg_UnpackKeywords()
+
+#if defined(HAVE_PTHREAD_GETNAME_NP)
+
+PyDoc_STRVAR(_thread__get_name__doc__,
+"_get_name($module, /)\n"
+"--\n"
+"\n"
+"Get the name of the current thread.");
+
+#define _THREAD__GET_NAME_METHODDEF    \
+    {"_get_name", (PyCFunction)_thread__get_name, METH_NOARGS, _thread__get_name__doc__},
+
+static PyObject *
+_thread__get_name_impl(PyObject *module);
+
+static PyObject *
+_thread__get_name(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+    return _thread__get_name_impl(module);
+}
+
+#endif /* defined(HAVE_PTHREAD_GETNAME_NP) */
+
+#if defined(HAVE_PTHREAD_SETNAME_NP)
+
+PyDoc_STRVAR(_thread_set_name__doc__,
+"set_name($module, /, name)\n"
+"--\n"
+"\n"
+"Set the name of the current thread.");
+
+#define _THREAD_SET_NAME_METHODDEF    \
+    {"set_name", _PyCFunction_CAST(_thread_set_name), METH_FASTCALL|METH_KEYWORDS, _thread_set_name__doc__},
+
+static PyObject *
+_thread_set_name_impl(PyObject *module, PyObject *name_obj);
+
+static PyObject *
+_thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_item = { &_Py_ID(name), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"name", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "set_name",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    PyObject *name_obj;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[0])) {
+        _PyArg_BadArgument("set_name", "argument 'name'", "str", args[0]);
+        goto exit;
+    }
+    name_obj = args[0];
+    return_value = _thread_set_name_impl(module, name_obj);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(HAVE_PTHREAD_SETNAME_NP) */
+
+#ifndef _THREAD__GET_NAME_METHODDEF
+    #define _THREAD__GET_NAME_METHODDEF
+#endif /* !defined(_THREAD__GET_NAME_METHODDEF) */
+
+#ifndef _THREAD_SET_NAME_METHODDEF
+    #define _THREAD_SET_NAME_METHODDEF
+#endif /* !defined(_THREAD_SET_NAME_METHODDEF) */
+/*[clinic end generated code: output=b5cb85aaccc45bf6 input=a9049054013a1b77]*/
index 5e9bcb602d884e1bac282f1d67283969d7261a86..bcbab8dfcff19046767dc6b635b12da57b7fdbfe 100755 (executable)
--- a/configure
+++ b/configure
@@ -821,6 +821,7 @@ MODULE_TIME_TRUE
 MODULE__IO_FALSE
 MODULE__IO_TRUE
 MODULE_BUILDTYPE
+PYTHREAD_NAME_MAXLEN
 TEST_MODULES
 OPENSSL_LDFLAGS
 OPENSSL_LIBS
@@ -18841,6 +18842,18 @@ if test "x$ac_cv_func_pthread_kill" = xyes
 then :
   printf "%s\n" "#define HAVE_PTHREAD_KILL 1" >>confdefs.h
 
+fi
+ac_fn_c_check_func "$LINENO" "pthread_getname_np" "ac_cv_func_pthread_getname_np"
+if test "x$ac_cv_func_pthread_getname_np" = xyes
+then :
+  printf "%s\n" "#define HAVE_PTHREAD_GETNAME_NP 1" >>confdefs.h
+
+fi
+ac_fn_c_check_func "$LINENO" "pthread_setname_np" "ac_cv_func_pthread_setname_np"
+if test "x$ac_cv_func_pthread_setname_np" = xyes
+then :
+  printf "%s\n" "#define HAVE_PTHREAD_SETNAME_NP 1" >>confdefs.h
+
 fi
 ac_fn_c_check_func "$LINENO" "ptsname" "ac_cv_func_ptsname"
 if test "x$ac_cv_func_ptsname" = xyes
 CPPFLAGS=$save_CPPFLAGS
 
 
+# gh-59705: Maximum length in bytes of a thread name
+case "$ac_sys_system" in
+  Linux*) PYTHREAD_NAME_MAXLEN=15;;  # Linux and Android
+  SunOS*) PYTHREAD_NAME_MAXLEN=31;;
+  Darwin) PYTHREAD_NAME_MAXLEN=63;;
+  iOS) PYTHREAD_NAME_MAXLEN=63;;
+  FreeBSD*) PYTHREAD_NAME_MAXLEN=98;;
+  *) PYTHREAD_NAME_MAXLEN=;;
+esac
+if test -n "$PYTHREAD_NAME_MAXLEN"; then
+
+printf "%s\n" "#define PYTHREAD_NAME_MAXLEN $PYTHREAD_NAME_MAXLEN" >>confdefs.h
+
+fi
+
+
+
 # stdlib
 
 
index bf3685e1b1b209d515d44fb3713b8cfa406c5b7f..922a125ea9608e51708ca50e9e0f4ad8f28bb411 100644 (file)
@@ -5110,8 +5110,10 @@ AC_CHECK_FUNCS([ \
   mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \
   pipe2 plock poll posix_fadvise posix_fallocate posix_openpt posix_spawn posix_spawnp \
   posix_spawn_file_actions_addclosefrom_np \
-  pread preadv preadv2 process_vm_readv pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
-  pthread_kill ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
+  pread preadv preadv2 process_vm_readv \
+  pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
+  pthread_kill pthread_getname_np pthread_setname_np \
+  ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
   rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \
   sched_setparam sched_setscheduler sem_clockwait sem_getvalue sem_open \
   sem_timedwait sem_unlink sendfile setegid seteuid setgid sethostname \
@@ -7498,6 +7500,22 @@ AS_VAR_IF([ac_cv_libatomic_needed], [yes],
 _RESTORE_VAR([CPPFLAGS])
 
 
+# gh-59705: Maximum length in bytes of a thread name
+case "$ac_sys_system" in
+  Linux*) PYTHREAD_NAME_MAXLEN=15;;  # Linux and Android
+  SunOS*) PYTHREAD_NAME_MAXLEN=31;;
+  Darwin) PYTHREAD_NAME_MAXLEN=63;;
+  iOS) PYTHREAD_NAME_MAXLEN=63;;
+  FreeBSD*) PYTHREAD_NAME_MAXLEN=98;;
+  *) PYTHREAD_NAME_MAXLEN=;;
+esac
+if test -n "$PYTHREAD_NAME_MAXLEN"; then
+    AC_DEFINE_UNQUOTED([PYTHREAD_NAME_MAXLEN], [$PYTHREAD_NAME_MAXLEN],
+                       [Maximum length in bytes of a thread name])
+fi
+AC_SUBST([PYTHREAD_NAME_MAXLEN])
+
+
 # stdlib
 AC_DEFUN([PY_STDLIB_MOD_SET_NA], [
   m4_foreach([mod], [$@], [
index 6a1f1284650b9f94dd70589a15b06edadafc413f..166c195a8c66fc95626d03c4de4ee961e031d975 100644 (file)
 /* Define to 1 if you have the `pthread_getcpuclockid' function. */
 #undef HAVE_PTHREAD_GETCPUCLOCKID
 
+/* Define to 1 if you have the `pthread_getname_np' function. */
+#undef HAVE_PTHREAD_GETNAME_NP
+
 /* Define to 1 if you have the <pthread.h> header file. */
 #undef HAVE_PTHREAD_H
 
 /* Define to 1 if you have the `pthread_kill' function. */
 #undef HAVE_PTHREAD_KILL
 
+/* Define to 1 if you have the `pthread_setname_np' function. */
+#undef HAVE_PTHREAD_SETNAME_NP
+
 /* Define to 1 if you have the `pthread_sigmask' function. */
 #undef HAVE_PTHREAD_SIGMASK
 
 /* Define as the preferred size in bits of long digits */
 #undef PYLONG_BITS_IN_DIGIT
 
+/* Maximum length in bytes of a thread name */
+#undef PYTHREAD_NAME_MAXLEN
+
 /* enabled builtin hash modules */
 #undef PY_BUILTIN_HASHLIB_HASHES