From c6d324d75d488b6646dd04b72f13a858e4fefb19 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 27 Nov 2024 17:03:13 +0100 Subject: [PATCH 01/30] gh-59705: Add _thread.set_name() function On Linux, threading.Thread now sets the thread name to the operating system. configure now checks if pthread_setname_np() function is available. --- Lib/threading.py | 7 ++ ...4-11-27-17-04-38.gh-issue-59705.sAGyvs.rst | 2 + Modules/_threadmodule.c | 31 +++++++ Modules/clinic/_threadmodule.c.h | 86 +++++++++++++++++++ configure | 6 ++ configure.ac | 6 +- pyconfig.h.in | 3 + 7 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-11-27-17-04-38.gh-issue-59705.sAGyvs.rst create mode 100644 Modules/clinic/_threadmodule.c.h diff --git a/Lib/threading.py b/Lib/threading.py index 94ea2f08178369..e7365790cc09f1 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -48,6 +48,11 @@ __all__.append('get_native_id') except AttributeError: _HAVE_THREAD_NATIVE_ID = False +try: + _set_name = _thread.set_name + _HAVE_SET_NAME = True +except AttributeError: + _HAVE_SET_NAME = False ThreadError = _thread.error try: _CRLock = _thread.RLock @@ -1027,6 +1032,8 @@ def _bootstrap_inner(self): self._set_ident() if _HAVE_THREAD_NATIVE_ID: self._set_native_id() + if _HAVE_SET_NAME and self._name: + _set_name(self._name) 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 index 00000000000000..a8c7b3d00755e6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-27-17-04-38.gh-issue-59705.sAGyvs.rst @@ -0,0 +1,2 @@ +On Linux, :class:`threading.Thread` now sets the thread name to the +operating system. Patch by Victor Stinner. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index f2a420ac1c589d..4fe5c7cdbb3fb5 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -17,6 +17,8 @@ # include // 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,27 @@ 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_SETNAME_NP +/*[clinic input] +_thread.set_name + + name: str + +Set the name of the current thread. +[clinic start generated code]*/ + +static PyObject * +_thread_set_name_impl(PyObject *module, const char *name) +/*[clinic end generated code: output=f051ce549bcd6b8e input=7e29bbdbfb046a04]*/ +{ + pthread_t thread = pthread_self(); + pthread_setname_np(thread, name); + 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 +2423,7 @@ 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 {NULL, NULL} /* sentinel */ }; diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h new file mode 100644 index 00000000000000..877ecfbedaf4b7 --- /dev/null +++ b/Modules/clinic/_threadmodule.c.h @@ -0,0 +1,86 @@ +/*[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_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, const char *name); + +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]; + const char *name; + + 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; + } + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(args[0], &name_length); + if (name == NULL) { + goto exit; + } + if (strlen(name) != (size_t)name_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + return_value = _thread_set_name_impl(module, name); + +exit: + return return_value; +} + +#endif /* defined(HAVE_PTHREAD_SETNAME_NP) */ + +#ifndef _THREAD_SET_NAME_METHODDEF + #define _THREAD_SET_NAME_METHODDEF +#endif /* !defined(_THREAD_SET_NAME_METHODDEF) */ +/*[clinic end generated code: output=bfb5c47ecc9e2cb8 input=a9049054013a1b77]*/ diff --git a/configure b/configure index 84b74ac3584bcd..c57b493070872f 100755 --- a/configure +++ b/configure @@ -18832,6 +18832,12 @@ 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_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 diff --git a/configure.ac b/configure.ac index 8fa6cb60900ad1..d7b32e8299a01f 100644 --- a/configure.ac +++ b/configure.ac @@ -5105,8 +5105,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_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 \ diff --git a/pyconfig.h.in b/pyconfig.h.in index 924d86627b0e9b..74ec9eb5fc5ed9 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -990,6 +990,9 @@ /* 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 From 63b5d52518685123cc3d7274f2f9abb26376558c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 27 Nov 2024 17:21:38 +0100 Subject: [PATCH 02/30] Port to macOS --- Modules/_threadmodule.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 4fe5c7cdbb3fb5..ff814236d0fe87 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2377,8 +2377,12 @@ static PyObject * _thread_set_name_impl(PyObject *module, const char *name) /*[clinic end generated code: output=f051ce549bcd6b8e input=7e29bbdbfb046a04]*/ { +#ifdef __APPLE__ + pthread_setname_np(name); +#else pthread_t thread = pthread_self(); pthread_setname_np(thread, name); +#endif Py_RETURN_NONE; } #endif // HAVE_PTHREAD_SETNAME_NP From 9f6a8ab3edb577a1dda44ec47734980eb39c193e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 27 Nov 2024 17:34:36 +0100 Subject: [PATCH 03/30] Add tests --- Lib/test/test_threading.py | 20 ++++++++++++++ Modules/_threadmodule.c | 33 ++++++++++++++++++++--- Modules/clinic/_threadmodule.c.h | 46 +++++++++++++++++++++----------- configure | 6 +++++ configure.ac | 2 +- pyconfig.h.in | 3 +++ 6 files changed, 91 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index b666533466e578..a2525044db018b 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2103,6 +2103,26 @@ def test__all__(self): support.check__all__(self, threading, ('threading', '_thread'), extra=extra, not_exported=not_exported) + def test_set_name(self): + try: + get_name = _thread._get_name + set_name = _thread.set_name + except AttributeError: + self.skipTest("need thread._get_name() and thread.set_name()") + + work_name = None + def work(): + nonlocal work_name + work_name = get_name() + + # name not too long to fit into Linux 15 bytes limit + name = "CustomName" + + thread = threading.Thread(target=work, name=name) + thread.start() + thread.join() + self.assertEqual(work_name, name) + class InterruptMainTests(unittest.TestCase): def check_interrupt_main_with_signal_handler(self, signum): diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index ff814236d0fe87..4e36e0713a42c9 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2364,19 +2364,45 @@ Internal only. Return a non-zero integer that uniquely identifies the main threa of the main interpreter."); +#ifdef HAVE_PTHREAD_SETNAME_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]*/ +{ + char name[17]; + size_t size = Py_ARRAY_LENGTH(name) - 1; +#ifdef __APPLE__ + pthread_getname_np(name, size); +#else + pthread_t thread = pthread_self(); + pthread_getname_np(thread, name, size); +#endif + name[size] = 0; + return PyUnicode_DecodeFSDefault(name); +} +#endif // HAVE_PTHREAD_SETNAME_NP + + #ifdef HAVE_PTHREAD_SETNAME_NP /*[clinic input] _thread.set_name - name: str + name as name_obj: object(converter="PyUnicode_FSConverter") Set the name of the current thread. [clinic start generated code]*/ static PyObject * -_thread_set_name_impl(PyObject *module, const char *name) -/*[clinic end generated code: output=f051ce549bcd6b8e input=7e29bbdbfb046a04]*/ +_thread_set_name_impl(PyObject *module, PyObject *name_obj) +/*[clinic end generated code: output=402b0c68e0c0daed input=a0459bd64f771808]*/ { + const char *name = PyBytes_AS_STRING(name_obj); #ifdef __APPLE__ pthread_setname_np(name); #else @@ -2428,6 +2454,7 @@ static PyMethodDef thread_methods[] = { {"_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 */ }; diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h index 877ecfbedaf4b7..f638c29fdd28e2 100644 --- a/Modules/clinic/_threadmodule.c.h +++ b/Modules/clinic/_threadmodule.c.h @@ -10,6 +10,28 @@ preserve #if defined(HAVE_PTHREAD_SETNAME_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_SETNAME_NP) */ + +#if defined(HAVE_PTHREAD_SETNAME_NP) + PyDoc_STRVAR(_thread_set_name__doc__, "set_name($module, /, name)\n" "--\n" @@ -20,7 +42,7 @@ PyDoc_STRVAR(_thread_set_name__doc__, {"set_name", _PyCFunction_CAST(_thread_set_name), METH_FASTCALL|METH_KEYWORDS, _thread_set_name__doc__}, static PyObject * -_thread_set_name_impl(PyObject *module, const char *name); +_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) @@ -52,27 +74,17 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb }; #undef KWTUPLE PyObject *argsbuf[1]; - const char *name; + 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]); + if (!PyUnicode_FSConverter(args[0], &name_obj)) { goto exit; } - Py_ssize_t name_length; - name = PyUnicode_AsUTF8AndSize(args[0], &name_length); - if (name == NULL) { - goto exit; - } - if (strlen(name) != (size_t)name_length) { - PyErr_SetString(PyExc_ValueError, "embedded null character"); - goto exit; - } - return_value = _thread_set_name_impl(module, name); + return_value = _thread_set_name_impl(module, name_obj); exit: return return_value; @@ -80,7 +92,11 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #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=bfb5c47ecc9e2cb8 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=6611486fd37f22bf input=a9049054013a1b77]*/ diff --git a/configure b/configure index c57b493070872f..f14eb5f04b780c 100755 --- a/configure +++ b/configure @@ -18832,6 +18832,12 @@ 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 diff --git a/configure.ac b/configure.ac index d7b32e8299a01f..caeeef905f3663 100644 --- a/configure.ac +++ b/configure.ac @@ -5107,7 +5107,7 @@ AC_CHECK_FUNCS([ \ posix_spawn_file_actions_addclosefrom_np \ pread preadv preadv2 process_vm_readv \ pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \ - pthread_kill pthread_setname_np \ + 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 \ diff --git a/pyconfig.h.in b/pyconfig.h.in index 74ec9eb5fc5ed9..f9c1235c837c67 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -981,6 +981,9 @@ /* 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 header file. */ #undef HAVE_PTHREAD_H From d79e7af7cfcf59f088f10f3300a621bf218ca13b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 27 Nov 2024 17:51:17 +0100 Subject: [PATCH 04/30] Try to fix macOS _get_name() --- Modules/_threadmodule.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 4e36e0713a42c9..ecdfd6a25dcfc8 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2377,12 +2377,8 @@ _thread__get_name_impl(PyObject *module) { char name[17]; size_t size = Py_ARRAY_LENGTH(name) - 1; -#ifdef __APPLE__ - pthread_getname_np(name, size); -#else pthread_t thread = pthread_self(); pthread_getname_np(thread, name, size); -#endif name[size] = 0; return PyUnicode_DecodeFSDefault(name); } From ebd97522d2b7428d66409a390443ff63811512f6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 11:12:17 +0100 Subject: [PATCH 05/30] Truncate to 15 bytes; add error handling --- Modules/_threadmodule.c | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index ecdfd6a25dcfc8..1768e5527f1aa4 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2378,7 +2378,12 @@ _thread__get_name_impl(PyObject *module) char name[17]; size_t size = Py_ARRAY_LENGTH(name) - 1; pthread_t thread = pthread_self(); - pthread_getname_np(thread, name, size); + int rc = pthread_getname_np(thread, name, size); + if (rc) { + errno = rc; + return PyErr_SetFromErrno(PyExc_OSError); + } + name[size] = 0; return PyUnicode_DecodeFSDefault(name); } @@ -2400,11 +2405,27 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) { const char *name = PyBytes_AS_STRING(name_obj); #ifdef __APPLE__ - pthread_setname_np(name); + int rc = pthread_setname_np(name); #else + +#if defined(__linux__) + // Truncate to 16 bytes including the NUL byte + char buffer[16]; + size_t len = strlen(name); + if (len > 15) { + memcpy(buffer, name, 15); + buffer[15] = 0; + name = buffer; + } +#endif + pthread_t thread = pthread_self(); - pthread_setname_np(thread, name); + int rc = pthread_setname_np(thread, name); #endif + if (rc) { + errno = rc; + return PyErr_SetFromErrno(PyExc_OSError); + } Py_RETURN_NONE; } #endif // HAVE_PTHREAD_SETNAME_NP From a7f565186ab018cd679dd55850dfa5e2a3cfa6dc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 15:36:19 +0100 Subject: [PATCH 06/30] Address review --- Lib/threading.py | 5 ++--- Modules/_threadmodule.c | 4 ++-- Modules/clinic/_threadmodule.c.h | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/threading.py b/Lib/threading.py index e7365790cc09f1..e8ae5b156a5db9 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -50,9 +50,8 @@ _HAVE_THREAD_NATIVE_ID = False try: _set_name = _thread.set_name - _HAVE_SET_NAME = True except AttributeError: - _HAVE_SET_NAME = False + _set_name = None ThreadError = _thread.error try: _CRLock = _thread.RLock @@ -1032,7 +1031,7 @@ def _bootstrap_inner(self): self._set_ident() if _HAVE_THREAD_NATIVE_ID: self._set_native_id() - if _HAVE_SET_NAME and self._name: + if _set_name is not None and self._name: _set_name(self._name) self._started.set() with _active_limbo_lock: diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 1768e5527f1aa4..77e905980d642a 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2364,7 +2364,7 @@ Internal only. Return a non-zero integer that uniquely identifies the main threa of the main interpreter."); -#ifdef HAVE_PTHREAD_SETNAME_NP +#ifdef HAVE_PTHREAD_GETNAME_NP /*[clinic input] _thread._get_name @@ -2387,7 +2387,7 @@ _thread__get_name_impl(PyObject *module) name[size] = 0; return PyUnicode_DecodeFSDefault(name); } -#endif // HAVE_PTHREAD_SETNAME_NP +#endif // HAVE_PTHREAD_GETNAME_NP #ifdef HAVE_PTHREAD_SETNAME_NP diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h index f638c29fdd28e2..52f4ef9c1008a9 100644 --- a/Modules/clinic/_threadmodule.c.h +++ b/Modules/clinic/_threadmodule.c.h @@ -8,7 +8,7 @@ preserve #endif #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() -#if defined(HAVE_PTHREAD_SETNAME_NP) +#if defined(HAVE_PTHREAD_GETNAME_NP) PyDoc_STRVAR(_thread__get_name__doc__, "_get_name($module, /)\n" @@ -28,7 +28,7 @@ _thread__get_name(PyObject *module, PyObject *Py_UNUSED(ignored)) return _thread__get_name_impl(module); } -#endif /* defined(HAVE_PTHREAD_SETNAME_NP) */ +#endif /* defined(HAVE_PTHREAD_GETNAME_NP) */ #if defined(HAVE_PTHREAD_SETNAME_NP) @@ -99,4 +99,4 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #ifndef _THREAD_SET_NAME_METHODDEF #define _THREAD_SET_NAME_METHODDEF #endif /* !defined(_THREAD_SET_NAME_METHODDEF) */ -/*[clinic end generated code: output=6611486fd37f22bf input=a9049054013a1b77]*/ +/*[clinic end generated code: output=82f504f284712f68 input=a9049054013a1b77]*/ From 97ea6452f06e9728197dcaff0105d4c20ebe6a65 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 15:52:26 +0100 Subject: [PATCH 07/30] Add test on non-ASCII name truncation --- Lib/test/test_threading.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index a2525044db018b..6e6e465a4ac985 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2106,22 +2106,43 @@ def test__all__(self): def test_set_name(self): try: get_name = _thread._get_name - set_name = _thread.set_name + _thread.set_name except AttributeError: self.skipTest("need thread._get_name() and thread.set_name()") - work_name = None def work(): nonlocal work_name work_name = get_name() # name not too long to fit into Linux 15 bytes limit name = "CustomName" + tests = [(name, name)] - thread = threading.Thread(target=work, name=name) - thread.start() - thread.join() - self.assertEqual(work_name, name) + if sys.platform == "linux": + # On Linux, set_name() truncates the name to 15 bytes. + + # Test ASCII name + name = "x" * 100 + tests.append((name, name[:15])) + + # Test non-ASCII name + name = "x" * 14 + "é€" + try: + encoded = os.fsencode(name) + except UnicodeEncodeError: + # name cannot be encoded to the filesystem encoding + pass + else: + expected = os.fsdecode(encoded[:15]) + tests.append((name, expected)) + + for name, expected in tests: + 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) class InterruptMainTests(unittest.TestCase): From 78a9ab90d5e27b2f5f716026c508dae4f3995cee Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 15:55:42 +0100 Subject: [PATCH 08/30] Add test on non-ASCII name --- Lib/test/test_threading.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 6e6e465a4ac985..04fd217a07ba50 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2118,6 +2118,16 @@ def work(): name = "CustomName" tests = [(name, name)] + # Test non-ASCII short name + name = "namé€" + try: + os.fsencode(name) + except UnicodeEncodeError: + # name cannot be encoded to the filesystem encoding + pass + else: + tests.append((name, name)) + if sys.platform == "linux": # On Linux, set_name() truncates the name to 15 bytes. From dcf13f480fa71d8561240bd0a85754216b451a00 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 15:58:21 +0100 Subject: [PATCH 09/30] Test long name on non-Linux platforms --- Lib/test/test_threading.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 04fd217a07ba50..b1722cb78866d6 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2145,6 +2145,10 @@ def work(): else: expected = os.fsdecode(encoded[:15]) tests.append((name, expected)) + else: + # Test long name + name = "x" * 100 + tests.append((name, name)) for name, expected in tests: with self.subTest(name=name, expected=expected): From 6ea7e5adedb27766148de3c8686774c192936e5c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 16:55:46 +0100 Subject: [PATCH 10/30] macOS is limited to 63 bytes --- Lib/test/test_threading.py | 16 ++++++++++++---- Modules/_threadmodule.c | 26 +++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index b1722cb78866d6..bd7035b6fca121 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2129,21 +2129,29 @@ def work(): tests.append((name, name)) if sys.platform == "linux": - # On Linux, set_name() truncates the name to 15 bytes. + limit = 15 + elif sys.platform == "darwin": + limit = 63 + else: + limit = None + + if limit is not None: + # On Linux and macOS, set_name() truncates the name to, + # respectively, 15 and 63 bytes. # Test ASCII name name = "x" * 100 - tests.append((name, name[:15])) + tests.append((name, name[:limit])) # Test non-ASCII name - name = "x" * 14 + "é€" + name = "x" * (limit - 1) + "é€" try: encoded = os.fsencode(name) except UnicodeEncodeError: # name cannot be encoded to the filesystem encoding pass else: - expected = os.fsdecode(encoded[:15]) + expected = os.fsdecode(encoded[:limit]) tests.append((name, expected)) else: # Test long name diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 77e905980d642a..8cc532913a9e41 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2375,7 +2375,8 @@ static PyObject * _thread__get_name_impl(PyObject *module) /*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/ { - char name[17]; + // Linux and macOS are limited to respectively 16 and 64 bytes + char name[100]; size_t size = Py_ARRAY_LENGTH(name) - 1; pthread_t thread = pthread_self(); int rc = pthread_getname_np(thread, name, size); @@ -2405,20 +2406,25 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) { const char *name = PyBytes_AS_STRING(name_obj); #ifdef __APPLE__ - int rc = pthread_setname_np(name); -#else +# define NAME_LIMIT 63 +#elif defined(__linux__) +# define NAME_LIMIT 15 +#endif -#if defined(__linux__) - // Truncate to 16 bytes including the NUL byte - char buffer[16]; +#ifdef NAME_LIMIT + // Truncate to NAME_LIMIT bytes + the NUL byte + char buffer[NAME_LIMIT + 1]; size_t len = strlen(name); - if (len > 15) { - memcpy(buffer, name, 15); - buffer[15] = 0; + if (len > NAME_LIMIT) { + memcpy(buffer, name, NAME_LIMIT); + buffer[NAME_LIMIT] = 0; name = buffer; } #endif +#ifdef __APPLE__ + int rc = pthread_setname_np(name); +#else pthread_t thread = pthread_self(); int rc = pthread_setname_np(thread, name); #endif @@ -2427,6 +2433,8 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) return PyErr_SetFromErrno(PyExc_OSError); } Py_RETURN_NONE; + +#undef NAME_LIMIT } #endif // HAVE_PTHREAD_SETNAME_NP From 46721bb36aed95dded64c6d697916770a5623423 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 17:08:48 +0100 Subject: [PATCH 11/30] Catch UnicodeEncodeError when seting the name Refactor also tests. --- Lib/test/test_threading.py | 67 ++++++++++++++++++-------------------- Lib/threading.py | 7 +++- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index bd7035b6fca121..babda6b75e289e 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2114,57 +2114,52 @@ def work(): nonlocal work_name work_name = get_name() - # name not too long to fit into Linux 15 bytes limit - name = "CustomName" - tests = [(name, name)] - - # Test non-ASCII short name - name = "namé€" - try: - os.fsencode(name) - except UnicodeEncodeError: - # name cannot be encoded to the filesystem encoding - pass - else: - tests.append((name, name)) - + # On Linux and macOS, set_name() truncates the name to, + # respectively, 15 and 63 bytes. if sys.platform == "linux": - limit = 15 + truncate = 15 elif sys.platform == "darwin": - limit = 63 + truncate = 63 else: - limit = None - - if limit is not None: - # On Linux and macOS, set_name() truncates the name to, - # respectively, 15 and 63 bytes. + truncate = None + limit = truncate or 100 - # Test ASCII name - name = "x" * 100 - tests.append((name, name[:limit])) - - # Test non-ASCII name - name = "x" * (limit - 1) + "é€" + def create_test(name): try: encoded = os.fsencode(name) except UnicodeEncodeError: - # name cannot be encoded to the filesystem encoding - pass + expected = None else: - expected = os.fsdecode(encoded[:limit]) - tests.append((name, expected)) - else: - # Test long name - name = "x" * 100 - tests.append((name, name)) + if truncate is not None: + expected = os.fsdecode(encoded[:truncate]) + else: + expected = name + return (name, expected) + + tests = [ + # test short ASCII name + create_test("CustomName"), + + # test short non-ASCII name + create_test("namé€"), + # Test long ASCII names (not truncated) + create_test("x" * limit), + + # Test long ASCII names (truncated) + create_test("x" * (limit + 10)), + + # Test long non-ASCII name (truncated) + create_test("x" * (limit - 1) + "é€"), + ] for name, expected in tests: 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) + if expected is not None: + self.assertEqual(work_name, expected) class InterruptMainTests(unittest.TestCase): diff --git a/Lib/threading.py b/Lib/threading.py index e8ae5b156a5db9..bb892bb5a8b39a 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1032,7 +1032,12 @@ def _bootstrap_inner(self): if _HAVE_THREAD_NATIVE_ID: self._set_native_id() if _set_name is not None and self._name: - _set_name(self._name) + try: + _set_name(self._name) + except UnicodeEncodeError: + # cannot set the name if the name cannot be encoded + # to the filesystem encoding + pass self._started.set() with _active_limbo_lock: _active[self._ident] = self From 69621160bf45aee2dbeaee90f3c9535fdc791fe9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 18:17:26 +0100 Subject: [PATCH 12/30] Add tests --- Lib/test/test_threading.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index babda6b75e289e..00b44359b8ff4a 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2152,6 +2152,11 @@ def create_test(name): # Test long non-ASCII name (truncated) create_test("x" * (limit - 1) + "é€"), ] + if os_helper.FS_NONASCII: + tests.append(create_test(f"nonascii:{os_helper.FS_NONASCII}")) + if os_helper.TESTFN_UNENCODABLE: + tests.append(create_test(os_helper.TESTFN_UNENCODABLE)) + for name, expected in tests: with self.subTest(name=name, expected=expected): work_name = None From 5d27da0650b9cd7d8547bf75a1c7be3d7f017b38 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 28 Nov 2024 18:18:35 +0100 Subject: [PATCH 13/30] Use "replace" error handler --- Lib/test/test_threading.py | 16 ++++++---------- Modules/_threadmodule.c | 23 ++++++++++++++++++++--- Modules/clinic/_threadmodule.c.h | 6 ++---- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 00b44359b8ff4a..f69dc5499e6e0f 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2125,15 +2125,12 @@ def work(): limit = truncate or 100 def create_test(name): - try: - encoded = os.fsencode(name) - except UnicodeEncodeError: - expected = None + if truncate is not None: + encoding = sys.getfilesystemencoding() + encoded = name.encode(encoding, "replace") + expected = os.fsdecode(encoded[:truncate]) else: - if truncate is not None: - expected = os.fsdecode(encoded[:truncate]) - else: - expected = name + expected = name return (name, expected) tests = [ @@ -2163,8 +2160,7 @@ def create_test(name): thread = threading.Thread(target=work, name=name) thread.start() thread.join() - if expected is not None: - self.assertEqual(work_name, expected) + self.assertEqual(work_name, expected) class InterruptMainTests(unittest.TestCase): diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 8cc532913a9e41..2e71a14f6a4e73 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2395,16 +2395,33 @@ _thread__get_name_impl(PyObject *module) /*[clinic input] _thread.set_name - name as name_obj: object(converter="PyUnicode_FSConverter") + name as name_obj: object 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=a0459bd64f771808]*/ +/*[clinic end generated code: output=402b0c68e0c0daed input=b55d3d4279e2e831]*/ { - const char *name = PyBytes_AS_STRING(name_obj); + if (!PyUnicode_Check(name_obj)) { + PyErr_Format(PyExc_TypeError, "expected str, got %T", name_obj); + return NULL; + } + + const PyConfig *config = _Py_GetConfig(); + char *encoding = Py_EncodeLocale(config->filesystem_encoding, NULL); + if (encoding == NULL) { + return NULL; + } + + PyObject *name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace"); + PyMem_Free(encoding); + if (name_encoded == NULL) { + return NULL; + } + + const char *name = PyBytes_AS_STRING(name_encoded); #ifdef __APPLE__ # define NAME_LIMIT 63 #elif defined(__linux__) diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h index 52f4ef9c1008a9..8b87a9b6f2a428 100644 --- a/Modules/clinic/_threadmodule.c.h +++ b/Modules/clinic/_threadmodule.c.h @@ -81,9 +81,7 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb if (!args) { goto exit; } - if (!PyUnicode_FSConverter(args[0], &name_obj)) { - goto exit; - } + name_obj = args[0]; return_value = _thread_set_name_impl(module, name_obj); exit: @@ -99,4 +97,4 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #ifndef _THREAD_SET_NAME_METHODDEF #define _THREAD_SET_NAME_METHODDEF #endif /* !defined(_THREAD_SET_NAME_METHODDEF) */ -/*[clinic end generated code: output=82f504f284712f68 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=37809e7cbdb7f7ef input=a9049054013a1b77]*/ From b713910bccf6b95efda8a89ed13eb57a6fbb8305 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 29 Nov 2024 15:54:04 +0100 Subject: [PATCH 14/30] Address review --- Lib/threading.py | 4 +--- Modules/_threadmodule.c | 13 +++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Lib/threading.py b/Lib/threading.py index bb892bb5a8b39a..3abd22a2aa1b72 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1034,9 +1034,7 @@ def _bootstrap_inner(self): if _set_name is not None and self._name: try: _set_name(self._name) - except UnicodeEncodeError: - # cannot set the name if the name cannot be encoded - # to the filesystem encoding + except OSError: pass self._started.set() with _active_limbo_lock: diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 2e71a14f6a4e73..72c3e198eb12b6 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2410,8 +2410,16 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) } const PyConfig *config = _Py_GetConfig(); - char *encoding = Py_EncodeLocale(config->filesystem_encoding, NULL); - if (encoding == NULL) { + char *encoding; + int res = _Py_EncodeUTF8Ex(config->filesystem_encoding, &encoding, + NULL, NULL, 0, _Py_ERROR_STRICT); + if (res == -2) { + PyErr_Format(PyExc_RuntimeWarning, + "cannot encode filesystem_encoding"); + return NULL; + } + if (res < 0) { + PyErr_NoMemory(); return NULL; } @@ -2445,6 +2453,7 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) 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); From 5c20ea1a7f5526ee09a64de353b1e022b143cf8a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 29 Nov 2024 16:55:48 +0100 Subject: [PATCH 15/30] Use PyInterpreterState filesystem encoding --- Modules/_threadmodule.c | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 72c3e198eb12b6..a01321a43cfb33 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2409,22 +2409,12 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) return NULL; } - const PyConfig *config = _Py_GetConfig(); - char *encoding; - int res = _Py_EncodeUTF8Ex(config->filesystem_encoding, &encoding, - NULL, NULL, 0, _Py_ERROR_STRICT); - if (res == -2) { - PyErr_Format(PyExc_RuntimeWarning, - "cannot encode filesystem_encoding"); - return NULL; - } - if (res < 0) { - PyErr_NoMemory(); - return NULL; - } - - PyObject *name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace"); - PyMem_Free(encoding); + // Encode the thread name to the filesystem encoding using the "replace" + // error handler + PyInterpreterState *interp = _PyInterpreterState_GET(); + char *encoding = interp->unicode.fs_codec.encoding; + PyObject *name_encoded; + name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace"); if (name_encoded == NULL) { return NULL; } From 6088b37759f0ce6cc1414a092ee7c4814d182256 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 2 Dec 2024 15:53:40 +0100 Subject: [PATCH 16/30] FreeBSD truncates to 98 bytes silently --- Lib/test/test_threading.py | 2 ++ Modules/_threadmodule.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index f69dc5499e6e0f..e0bc9463e8b5ac 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2120,6 +2120,8 @@ def work(): truncate = 15 elif sys.platform == "darwin": truncate = 63 + elif sys.platform.startswith("freebsd"): + truncate = 98 else: truncate = None limit = truncate or 100 diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index a01321a43cfb33..733510c0160be4 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2424,6 +2424,8 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) # define NAME_LIMIT 63 #elif defined(__linux__) # define NAME_LIMIT 15 +#elif defined(__FreeBSD__) +# define NAME_LIMIT 98 #endif #ifdef NAME_LIMIT From bde935f684a8040b1a587df7ad79de31b8aeafca Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Dec 2024 09:19:17 +0100 Subject: [PATCH 17/30] Fix test_threading on iOS iOS also truncates to 63 bytes, same as macOS. --- Lib/test/test_threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index e0bc9463e8b5ac..2b9cd3c04cabe5 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2118,7 +2118,7 @@ def work(): # respectively, 15 and 63 bytes. if sys.platform == "linux": truncate = 15 - elif sys.platform == "darwin": + elif sys.platform in ("darwin", "ios"): truncate = 63 elif sys.platform.startswith("freebsd"): truncate = 98 From 0584ca3398c406b35ea4f295070397e220ddbbda Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Dec 2024 14:11:02 +0100 Subject: [PATCH 18/30] Fix create_test(): always encode using "replace" --- Lib/test/test_threading.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 2b9cd3c04cabe5..0f4dbb63c58738 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2114,8 +2114,7 @@ def work(): nonlocal work_name work_name = get_name() - # On Linux and macOS, set_name() truncates the name to, - # respectively, 15 and 63 bytes. + # set_name() limit in bytes if sys.platform == "linux": truncate = 15 elif sys.platform in ("darwin", "ios"): @@ -2127,12 +2126,12 @@ def work(): limit = truncate or 100 def create_test(name): + encoding = sys.getfilesystemencoding() + encoded = name.encode(encoding, "replace") if truncate is not None: - encoding = sys.getfilesystemencoding() - encoded = name.encode(encoding, "replace") expected = os.fsdecode(encoded[:truncate]) else: - expected = name + expected = os.fsdecode(encoded) return (name, expected) tests = [ @@ -2162,7 +2161,8 @@ def create_test(name): thread = threading.Thread(target=work, name=name) thread.start() thread.join() - self.assertEqual(work_name, expected) + self.assertEqual(work_name, expected, + f"{len(work_name)=} and {len(expected)=}") class InterruptMainTests(unittest.TestCase): From 7508b6cb2c83a47b38ac27b218a3fccba69bcc53 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Dec 2024 14:22:13 +0100 Subject: [PATCH 19/30] Solaris truncates to 31 bytes --- Lib/test/test_threading.py | 2 ++ Modules/_threadmodule.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 0f4dbb63c58738..22325495f9c67e 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2121,6 +2121,8 @@ def work(): truncate = 63 elif sys.platform.startswith("freebsd"): truncate = 98 + elif sys.platform.startswith("solaris"): + truncate = 31 else: truncate = None limit = truncate or 100 diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index e3962013d1c7e1..be8efe641bf6a9 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2426,6 +2426,8 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) # define NAME_LIMIT 15 #elif defined(__FreeBSD__) # define NAME_LIMIT 98 +#elif defined(__sun) +# define NAME_LIMIT 31 #endif #ifdef NAME_LIMIT From f4a9f401f7576f387aedcee856c254000b4a397d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Dec 2024 14:05:40 +0100 Subject: [PATCH 20/30] Solaris always uses UTF-8 --- Lib/test/test_threading.py | 5 ++++- Modules/_threadmodule.c | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 22325495f9c67e..0aa95c79d0c657 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2128,7 +2128,10 @@ def work(): limit = truncate or 100 def create_test(name): - encoding = sys.getfilesystemencoding() + if sys.platform.startswith("solaris"): + encoding = "utf-8" + else: + encoding = sys.getfilesystemencoding() encoded = name.encode(encoding, "replace") if truncate is not None: expected = os.fsdecode(encoded[:truncate]) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index be8efe641bf6a9..02f4254960bda2 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2409,10 +2409,15 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) return NULL; } +#ifdef __sun + // Solaris always uses UTF-8 + char *encoding = "utf-8"; +#else // Encode the thread name to the filesystem encoding using the "replace" // error handler PyInterpreterState *interp = _PyInterpreterState_GET(); char *encoding = interp->unicode.fs_codec.encoding; +#endif PyObject *name_encoded; name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace"); if (name_encoded == NULL) { From ac6d7263489b665b9d5b1c8e7433bcf58213fcd0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Dec 2024 14:32:29 +0100 Subject: [PATCH 21/30] Add PYTHREAD_NAME_MAXLEN macro Add also _thread._NAME_MAXLEN constant for test_threading. --- Lib/test/test_threading.py | 11 +---------- Modules/_threadmodule.c | 31 +++++++++++++------------------ configure | 18 ++++++++++++++++++ configure.ac | 16 ++++++++++++++++ pyconfig.h.in | 3 +++ 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 0aa95c79d0c657..16fffc8341ac05 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2115,16 +2115,7 @@ def work(): work_name = get_name() # set_name() limit in bytes - if sys.platform == "linux": - truncate = 15 - elif sys.platform in ("darwin", "ios"): - truncate = 63 - elif sys.platform.startswith("freebsd"): - truncate = 98 - elif sys.platform.startswith("solaris"): - truncate = 31 - else: - truncate = None + truncate = getattr(_thread, "_NAME_MAXLEN", None) limit = truncate or 100 def create_test(name): diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 02f4254960bda2..d184f81fb69e74 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2425,23 +2425,13 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) } const char *name = PyBytes_AS_STRING(name_encoded); -#ifdef __APPLE__ -# define NAME_LIMIT 63 -#elif defined(__linux__) -# define NAME_LIMIT 15 -#elif defined(__FreeBSD__) -# define NAME_LIMIT 98 -#elif defined(__sun) -# define NAME_LIMIT 31 -#endif - -#ifdef NAME_LIMIT - // Truncate to NAME_LIMIT bytes + the NUL byte - char buffer[NAME_LIMIT + 1]; +#ifdef PYTHREAD_NAME_MAXLEN + // Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte + char buffer[PYTHREAD_NAME_MAXLEN + 1]; size_t len = strlen(name); - if (len > NAME_LIMIT) { - memcpy(buffer, name, NAME_LIMIT); - buffer[NAME_LIMIT] = 0; + if (len > PYTHREAD_NAME_MAXLEN) { + memcpy(buffer, name, PYTHREAD_NAME_MAXLEN); + buffer[PYTHREAD_NAME_MAXLEN] = 0; name = buffer; } #endif @@ -2458,8 +2448,6 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) return PyErr_SetFromErrno(PyExc_OSError); } Py_RETURN_NONE; - -#undef NAME_LIMIT } #endif // HAVE_PTHREAD_SETNAME_NP @@ -2596,6 +2584,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/configure b/configure index e24418f4478f9a..4cea70d9d988f6 100755 --- 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 @@ -29086,6 +29087,23 @@ fi CPPFLAGS=$save_CPPFLAGS +# gh-59705: Maximum length in bytes of a thread name +case "$ac_sys_system" in + Linux*) PYTHREAD_NAME_MAXLEN=15;; + 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 diff --git a/configure.ac b/configure.ac index 5550acde958fab..1dda6c9590f519 100644 --- a/configure.ac +++ b/configure.ac @@ -7499,6 +7499,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;; + 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], [$@], [ diff --git a/pyconfig.h.in b/pyconfig.h.in index f9c1235c837c67..91d14669a90d62 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1653,6 +1653,9 @@ /* 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 From 08e5922f19355e3494f64ea0a5e656de88871a8e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 4 Dec 2024 14:43:36 +0100 Subject: [PATCH 22/30] Fix configure.ac if PYTHREAD_NAME_MAXLEN is not set --- configure | 2 +- configure.ac | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure b/configure index 4cea70d9d988f6..4d8db7941d0eb0 100755 --- a/configure +++ b/configure @@ -29096,7 +29096,7 @@ case "$ac_sys_system" in FreeBSD*) PYTHREAD_NAME_MAXLEN=98;; *) PYTHREAD_NAME_MAXLEN=;; esac -if test -n $PYTHREAD_NAME_MAXLEN; then +if test -n "$PYTHREAD_NAME_MAXLEN"; then printf "%s\n" "#define PYTHREAD_NAME_MAXLEN $PYTHREAD_NAME_MAXLEN" >>confdefs.h diff --git a/configure.ac b/configure.ac index 1dda6c9590f519..b1462cd42db3b0 100644 --- a/configure.ac +++ b/configure.ac @@ -7508,7 +7508,7 @@ case "$ac_sys_system" in FreeBSD*) PYTHREAD_NAME_MAXLEN=98;; *) PYTHREAD_NAME_MAXLEN=;; esac -if test -n $PYTHREAD_NAME_MAXLEN; then +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 From 681624e07b5f3f6e5d33a719c3a0b639253e5ce0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 5 Dec 2024 16:30:29 +0100 Subject: [PATCH 23/30] Address Serhiy's review --- Modules/_threadmodule.c | 30 +++++++++++++----------------- Modules/clinic/_threadmodule.c.h | 6 +++++- configure | 2 +- configure.ac | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index d184f81fb69e74..8dc38bc59517d2 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2377,15 +2377,13 @@ _thread__get_name_impl(PyObject *module) { // Linux and macOS are limited to respectively 16 and 64 bytes char name[100]; - size_t size = Py_ARRAY_LENGTH(name) - 1; pthread_t thread = pthread_self(); - int rc = pthread_getname_np(thread, name, size); + int rc = pthread_getname_np(thread, name, Py_ARRAY_LENGTH(name)); if (rc) { errno = rc; return PyErr_SetFromErrno(PyExc_OSError); } - name[size] = 0; return PyUnicode_DecodeFSDefault(name); } #endif // HAVE_PTHREAD_GETNAME_NP @@ -2395,28 +2393,23 @@ _thread__get_name_impl(PyObject *module) /*[clinic input] _thread.set_name - name as name_obj: object + 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=b55d3d4279e2e831]*/ +/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/ { - if (!PyUnicode_Check(name_obj)) { - PyErr_Format(PyExc_TypeError, "expected str, got %T", name_obj); - return NULL; - } - #ifdef __sun // Solaris always uses UTF-8 - char *encoding = "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(); - char *encoding = interp->unicode.fs_codec.encoding; + const char *encoding = interp->unicode.fs_codec.encoding; #endif PyObject *name_encoded; name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace"); @@ -2426,13 +2419,16 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) const char *name = PyBytes_AS_STRING(name_encoded); #ifdef PYTHREAD_NAME_MAXLEN - // Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte - char buffer[PYTHREAD_NAME_MAXLEN + 1]; + // Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed size_t len = strlen(name); if (len > PYTHREAD_NAME_MAXLEN) { - memcpy(buffer, name, PYTHREAD_NAME_MAXLEN); - buffer[PYTHREAD_NAME_MAXLEN] = 0; - name = buffer; + PyObject *truncated = PyBytes_FromStringAndSize(name, PYTHREAD_NAME_MAXLEN); + if (truncated == NULL) { + Py_DECREF(name_encoded); + return NULL; + } + Py_SETREF(name_encoded, truncated); + name = PyBytes_AS_STRING(name_encoded); } #endif diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h index 8b87a9b6f2a428..8f0507d40285b3 100644 --- a/Modules/clinic/_threadmodule.c.h +++ b/Modules/clinic/_threadmodule.c.h @@ -81,6 +81,10 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb 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); @@ -97,4 +101,4 @@ _thread_set_name(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyOb #ifndef _THREAD_SET_NAME_METHODDEF #define _THREAD_SET_NAME_METHODDEF #endif /* !defined(_THREAD_SET_NAME_METHODDEF) */ -/*[clinic end generated code: output=37809e7cbdb7f7ef input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b5cb85aaccc45bf6 input=a9049054013a1b77]*/ diff --git a/configure b/configure index 4d8db7941d0eb0..1f3787fa9a1a1f 100755 --- a/configure +++ b/configure @@ -29089,7 +29089,7 @@ CPPFLAGS=$save_CPPFLAGS # gh-59705: Maximum length in bytes of a thread name case "$ac_sys_system" in - Linux*) PYTHREAD_NAME_MAXLEN=15;; + Linux*) PYTHREAD_NAME_MAXLEN=15;; # Linux and Android SunOS*) PYTHREAD_NAME_MAXLEN=31;; Darwin) PYTHREAD_NAME_MAXLEN=63;; iOS) PYTHREAD_NAME_MAXLEN=63;; diff --git a/configure.ac b/configure.ac index b1462cd42db3b0..a0f3fed8d11ef7 100644 --- a/configure.ac +++ b/configure.ac @@ -7501,7 +7501,7 @@ _RESTORE_VAR([CPPFLAGS]) # gh-59705: Maximum length in bytes of a thread name case "$ac_sys_system" in - Linux*) PYTHREAD_NAME_MAXLEN=15;; + Linux*) PYTHREAD_NAME_MAXLEN=15;; # Linux and Android SunOS*) PYTHREAD_NAME_MAXLEN=31;; Darwin) PYTHREAD_NAME_MAXLEN=63;; iOS) PYTHREAD_NAME_MAXLEN=63;; From f57339f3d6096e1b026f26a41c9fa76d02ba88b1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 10:25:44 +0100 Subject: [PATCH 24/30] Solaris always use UTF-8 --- Modules/_threadmodule.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 8dc38bc59517d2..c8f1a3112b3a9a 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2384,7 +2384,11 @@ _thread__get_name_impl(PyObject *module) 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 From 3941123b2ede5872d0e18b54c91dd6df900891a4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 10:25:56 +0100 Subject: [PATCH 25/30] Simplify tests --- Lib/test/test_threading.py | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 16fffc8341ac05..bfdb2544f38eba 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2118,40 +2118,39 @@ def work(): truncate = getattr(_thread, "_NAME_MAXLEN", None) limit = truncate or 100 - def create_test(name): - if sys.platform.startswith("solaris"): - encoding = "utf-8" - else: - encoding = sys.getfilesystemencoding() - encoded = name.encode(encoding, "replace") - if truncate is not None: - expected = os.fsdecode(encoded[:truncate]) - else: - expected = os.fsdecode(encoded) - return (name, expected) - tests = [ # test short ASCII name - create_test("CustomName"), + "CustomName", # test short non-ASCII name - create_test("namé€"), + "namé€", # Test long ASCII names (not truncated) - create_test("x" * limit), + "x" * limit, # Test long ASCII names (truncated) - create_test("x" * (limit + 10)), + "x" * (limit + 10), # Test long non-ASCII name (truncated) - create_test("x" * (limit - 1) + "é€"), + "x" * (limit - 1) + "é€", ] if os_helper.FS_NONASCII: - tests.append(create_test(f"nonascii:{os_helper.FS_NONASCII}")) + tests.append(f"nonascii:{os_helper.FS_NONASCII}") if os_helper.TESTFN_UNENCODABLE: - tests.append(create_test(os_helper.TESTFN_UNENCODABLE)) + tests.append(os_helper.TESTFN_UNENCODABLE) + + if sys.platform.startswith("solaris"): + encoding = "utf-8" + else: + encoding = sys.getfilesystemencoding() + + for name in tests: + encoded = name.encode(encoding, "replace") + if truncate is not None: + expected = os.fsdecode(encoded[:truncate]) + else: + expected = os.fsdecode(encoded) - for name, expected in tests: with self.subTest(name=name, expected=expected): work_name = None thread = threading.Thread(target=work, name=name) From 2e270435262491e458cbdcdaa3c23bce53f15b16 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 10:29:09 +0100 Subject: [PATCH 26/30] Use @unittest.skipUnless --- Lib/test/test_threading.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index bfdb2544f38eba..cae1d5b8b7cf38 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2103,17 +2103,9 @@ def test__all__(self): 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): - try: - get_name = _thread._get_name - _thread.set_name - except AttributeError: - self.skipTest("need thread._get_name() and thread.set_name()") - - def work(): - nonlocal work_name - work_name = get_name() - # set_name() limit in bytes truncate = getattr(_thread, "_NAME_MAXLEN", None) limit = truncate or 100 @@ -2144,6 +2136,10 @@ def work(): else: encoding = sys.getfilesystemencoding() + def work(): + nonlocal work_name + work_name = _thread._get_name() + for name in tests: encoded = name.encode(encoding, "replace") if truncate is not None: From 6ab29446495a6d558cf7c1bbbe4ac92208030a4d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 12:38:20 +0100 Subject: [PATCH 27/30] Solaris uses UTF-8 --- Lib/test/test_threading.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index cae1d5b8b7cf38..8994d1afd81a8a 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2143,7 +2143,9 @@ def work(): for name in tests: encoded = name.encode(encoding, "replace") if truncate is not None: - expected = os.fsdecode(encoded[:truncate]) + encoded = encoded[:truncate] + if sys.platform.startswith("solaris"): + expected = encoded.decode("utf-8") else: expected = os.fsdecode(encoded) From b370c49cb9a424150f1136f80a5d8bdd2642e4d6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 14:22:44 +0100 Subject: [PATCH 28/30] Update Lib/test/test_threading.py Co-authored-by: Serhiy Storchaka --- Lib/test/test_threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 8994d1afd81a8a..e951fcd23b91b7 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2145,7 +2145,7 @@ def work(): if truncate is not None: encoded = encoded[:truncate] if sys.platform.startswith("solaris"): - expected = encoded.decode("utf-8") + expected = encoded.decode("utf-8", "surrogateescape") else: expected = os.fsdecode(encoded) From beeae59ac36995d2d85e74244261527982279862 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 16:52:57 +0100 Subject: [PATCH 29/30] add test on embedded null character --- Lib/test/test_threading.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index e951fcd23b91b7..7d16b0a1a87f1a 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2117,6 +2117,10 @@ def test_set_name(self): # 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, @@ -2142,6 +2146,8 @@ def work(): 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"): From ae956a0149b3867e04e55a90508af816aa635014 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Dec 2024 16:53:07 +0100 Subject: [PATCH 30/30] Optimize code truncating the name --- Modules/_threadmodule.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index c8f1a3112b3a9a..35c032fbeaa94f 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2421,21 +2421,22 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj) return NULL; } - const char *name = PyBytes_AS_STRING(name_encoded); #ifdef PYTHREAD_NAME_MAXLEN // Truncate to PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed - size_t len = strlen(name); + size_t len = PyBytes_GET_SIZE(name_encoded); if (len > PYTHREAD_NAME_MAXLEN) { - PyObject *truncated = PyBytes_FromStringAndSize(name, 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); - name = PyBytes_AS_STRING(name_encoded); } #endif + const char *name = PyBytes_AS_STRING(name_encoded); #ifdef __APPLE__ int rc = pthread_setname_np(name); #else