Skip to content

Commit 8f2178d

Browse files
Support disallowing daemon threads in an interpreter.
1 parent 66486fd commit 8f2178d

File tree

11 files changed

+150
-10
lines changed

11 files changed

+150
-10
lines changed

Include/cpython/initconfig.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,14 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
246246
typedef struct {
247247
int allow_fork;
248248
int allow_threads;
249+
int allow_daemon_threads;
249250
} _PyInterpreterConfig;
250251

251252
#define _PyInterpreterConfig_LEGACY_INIT \
252253
{ \
253254
.allow_fork = 1, \
254255
.allow_threads = 1, \
256+
.allow_daemon_threads = 1, \
255257
}
256258

257259
/* --- Helper functions --------------------------------------- */

Include/cpython/pystate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ might not be allowed in the current interpreter (i.e. os.fork() would fail).
1414
/* Set if threads are allowed. */
1515
#define Py_RTFLAGS_THREADS (1UL << 10)
1616

17+
/* Set if daemon threads are allowed. */
18+
#define Py_RTFLAGS_DAEMON_THREADS (1UL << 11)
19+
1720
/* Set if os.fork() is allowed. */
1821
#define Py_RTFLAGS_FORK (1UL << 15)
1922

Lib/test/test__xxsubinterpreters.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ def f():
801801
self.assertEqual(out, 'it worked!')
802802

803803
def test_create_thread(self):
804-
subinterp = interpreters.create(isolated=False)
804+
subinterp = interpreters.create()
805805
script, file = _captured_script("""
806806
import threading
807807
def f():
@@ -817,6 +817,45 @@ def f():
817817

818818
self.assertEqual(out, 'it worked!')
819819

820+
def test_create_daemon_thread(self):
821+
with self.subTest('isolated'):
822+
expected = 'spam spam spam spam spam'
823+
subinterp = interpreters.create(isolated=True)
824+
script, file = _captured_script(f"""
825+
import threading
826+
def f():
827+
print('it worked!', end='')
828+
829+
try:
830+
t = threading.Thread(target=f, daemon=True)
831+
t.start()
832+
t.join()
833+
except RuntimeError:
834+
print('{expected}', end='')
835+
""")
836+
with file:
837+
interpreters.run_string(subinterp, script)
838+
out = file.read()
839+
840+
self.assertEqual(out, expected)
841+
842+
with self.subTest('not isolated'):
843+
subinterp = interpreters.create(isolated=False)
844+
script, file = _captured_script("""
845+
import threading
846+
def f():
847+
print('it worked!', end='')
848+
849+
t = threading.Thread(target=f, daemon=True)
850+
t.start()
851+
t.join()
852+
""")
853+
with file:
854+
interpreters.run_string(subinterp, script)
855+
out = file.read()
856+
857+
self.assertEqual(out, 'it worked!')
858+
820859
@support.requires_fork()
821860
def test_fork(self):
822861
import tempfile

Lib/test/test_capi.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,14 +1094,15 @@ def test_configured_settings(self):
10941094
import json
10951095

10961096
THREADS = 1<<10
1097+
DAEMON_THREADS = 1<<11
10971098
FORK = 1<<15
10981099

1099-
features = ['fork', 'threads']
1100+
features = ['fork', 'threads', 'daemon_threads']
11001101
kwlist = [f'allow_{n}' for n in features]
11011102
for config, expected in {
1102-
(True, True): FORK | THREADS,
1103-
(False, False): 0,
1104-
(False, True): THREADS,
1103+
(True, True, True): FORK | THREADS | DAEMON_THREADS,
1104+
(False, False, False): 0,
1105+
(False, True, False): THREADS,
11051106
}.items():
11061107
kwargs = dict(zip(kwlist, config))
11071108
expected = {

Lib/test/test_embed.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1649,10 +1649,11 @@ def test_init_use_frozen_modules(self):
16491649

16501650
def test_init_main_interpreter_settings(self):
16511651
THREADS = 1<<10
1652+
DAEMON_THREADS = 1<<11
16521653
FORK = 1<<15
16531654
expected = {
16541655
# All optional features should be enabled.
1655-
'feature_flags': THREADS | FORK,
1656+
'feature_flags': FORK | THREADS | DAEMON_THREADS,
16561657
}
16571658
out, err = self.run_embedded_interpreter(
16581659
'test_init_main_interpreter_settings',

Lib/test/test_threading.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,61 @@ def f():
13051305
self.assertIn("Fatal Python error: Py_EndInterpreter: "
13061306
"not the last thread", err.decode())
13071307

1308+
def _check_allowed(self, before_start='', *,
1309+
allowed=True,
1310+
daemon_allowed=True,
1311+
daemon=False,
1312+
):
1313+
subinterp_code = textwrap.dedent(f"""
1314+
import test.support
1315+
import threading
1316+
def func():
1317+
print('this should not have run!')
1318+
t = threading.Thread(target=func, daemon={daemon})
1319+
{before_start}
1320+
t.start()
1321+
""")
1322+
script = textwrap.dedent(f"""
1323+
import test.support
1324+
test.support.run_in_subinterp_with_config(
1325+
{subinterp_code!r},
1326+
allow_fork=True,
1327+
allow_threads={allowed},
1328+
allow_daemon_threads={daemon_allowed},
1329+
)
1330+
""")
1331+
with test.support.SuppressCrashReport():
1332+
_, _, err = assert_python_ok("-c", script)
1333+
return err.decode()
1334+
1335+
@cpython_only
1336+
def test_threads_not_allowed(self):
1337+
err = self._check_allowed(
1338+
allowed=False,
1339+
daemon_allowed=False,
1340+
daemon=False,
1341+
)
1342+
self.assertIn('RuntimeError', err)
1343+
1344+
@cpython_only
1345+
def test_daemon_threads_not_allowed(self):
1346+
with self.subTest('via Thread()'):
1347+
err = self._check_allowed(
1348+
allowed=True,
1349+
daemon_allowed=False,
1350+
daemon=True,
1351+
)
1352+
self.assertIn('RuntimeError', err)
1353+
1354+
with self.subTest('via Thread.daemon setter'):
1355+
err = self._check_allowed(
1356+
't.daemon = True',
1357+
allowed=True,
1358+
daemon_allowed=False,
1359+
daemon=False,
1360+
)
1361+
self.assertIn('RuntimeError', err)
1362+
13081363

13091364
class ThreadingExceptionTests(BaseTestCase):
13101365
# A RuntimeError should be raised if Thread.start() is called

Lib/threading.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
# Rename some stuff so "from threading import *" is safe
3535
_start_new_thread = _thread.start_new_thread
36+
_daemon_threads_allowed = _thread.daemon_threads_allowed
3637
_allocate_lock = _thread.allocate_lock
3738
_set_sentinel = _thread._set_sentinel
3839
get_ident = _thread.get_ident
@@ -899,6 +900,8 @@ class is implemented.
899900
self._args = args
900901
self._kwargs = kwargs
901902
if daemon is not None:
903+
if daemon and not _daemon_threads_allowed():
904+
raise RuntimeError('daemon threads are disabled in this interpreter')
902905
self._daemonic = daemon
903906
else:
904907
self._daemonic = current_thread().daemon
@@ -1226,6 +1229,8 @@ def daemon(self):
12261229
def daemon(self, daemonic):
12271230
if not self._initialized:
12281231
raise RuntimeError("Thread.__init__() not called")
1232+
if daemonic and not _daemon_threads_allowed():
1233+
raise RuntimeError('daemon threads are disabled in this interpreter')
12291234
if self._started.is_set():
12301235
raise RuntimeError("cannot set daemon status of active thread")
12311236
self._daemonic = daemonic
@@ -1432,7 +1437,8 @@ def __init__(self):
14321437
class _DummyThread(Thread):
14331438

14341439
def __init__(self):
1435-
Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
1440+
Thread.__init__(self, name=_newname("Dummy-%d"),
1441+
daemon=_daemon_threads_allowed())
14361442

14371443
self._started.set()
14381444
self._set_ident()

Modules/_testcapimodule.c

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,17 +3232,21 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32323232
const char *code;
32333233
int allow_fork = -1;
32343234
int allow_threads = -1;
3235+
int allow_daemon_threads = -1;
32353236
int r;
32363237
PyThreadState *substate, *mainstate;
32373238
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
32383239
PyCompilerFlags cflags = {0};
32393240

32403241
static char *kwlist[] = {"code",
3241-
"allow_fork", "allow_threads",
3242+
"allow_fork",
3243+
"allow_threads",
3244+
"allow_daemon_threads",
32423245
NULL};
32433246
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
3244-
"s$pp:run_in_subinterp_with_config", kwlist,
3245-
&code, &allow_fork, &allow_threads)) {
3247+
"s$ppp:run_in_subinterp_with_config", kwlist,
3248+
&code, &allow_fork,
3249+
&allow_threads, &allow_daemon_threads)) {
32463250
return NULL;
32473251
}
32483252
if (allow_fork < 0) {
@@ -3253,6 +3257,10 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32533257
PyErr_SetString(PyExc_ValueError, "missing allow_threads");
32543258
return NULL;
32553259
}
3260+
if (allow_daemon_threads < 0) {
3261+
PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads");
3262+
return NULL;
3263+
}
32563264

32573265
mainstate = PyThreadState_Get();
32583266

@@ -3261,6 +3269,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
32613269
const _PyInterpreterConfig config = {
32623270
.allow_fork = allow_fork,
32633271
.allow_threads = allow_threads,
3272+
.allow_daemon_threads = allow_daemon_threads,
32643273
};
32653274
substate = _Py_NewInterpreterFromConfig(&config);
32663275
if (substate == NULL) {

Modules/_threadmodule.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,24 @@ thread_run(void *boot_raw)
11021102
// to open the libgcc_s.so library (ex: EMFILE error).
11031103
}
11041104

1105+
static PyObject *
1106+
thread_daemon_threads_allowed(PyObject *module, PyObject *Py_UNUSED(ignored))
1107+
{
1108+
PyInterpreterState *interp = _PyInterpreterState_Get();
1109+
if (interp->feature_flags & Py_RTFLAGS_DAEMON_THREADS) {
1110+
Py_RETURN_TRUE;
1111+
}
1112+
else {
1113+
Py_RETURN_FALSE;
1114+
}
1115+
}
1116+
1117+
PyDoc_STRVAR(daemon_threads_allowed_doc,
1118+
"daemon_threads_allowed()\n\
1119+
\n\
1120+
Return True if daemon threads are allowed in the current interpreter,\n\
1121+
and False otherwise.\n");
1122+
11051123
static PyObject *
11061124
thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs)
11071125
{
@@ -1543,6 +1561,8 @@ static PyMethodDef thread_methods[] = {
15431561
METH_VARARGS, start_new_doc},
15441562
{"start_new", (PyCFunction)thread_PyThread_start_new_thread,
15451563
METH_VARARGS, start_new_doc},
1564+
{"daemon_threads_allowed", (PyCFunction)thread_daemon_threads_allowed,
1565+
METH_NOARGS, daemon_threads_allowed_doc},
15461566
{"allocate_lock", thread_PyThread_allocate_lock,
15471567
METH_NOARGS, allocate_doc},
15481568
{"allocate", thread_PyThread_allocate_lock,

Modules/_xxsubinterpretersmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,7 @@ interp_create(PyObject *self, PyObject *args, PyObject *kwds)
20062006
const _PyInterpreterConfig config = {
20072007
.allow_fork = !isolated,
20082008
.allow_threads = !isolated,
2009+
.allow_daemon_threads = !isolated,
20092010
};
20102011
// XXX Possible GILState issues?
20112012
PyThreadState *tstate = _Py_NewInterpreterFromConfig(&config);

Python/pylifecycle.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,9 @@ init_interp_settings(PyInterpreterState *interp, const _PyInterpreterConfig *con
621621
if (config->allow_threads) {
622622
interp->feature_flags |= Py_RTFLAGS_THREADS;
623623
}
624+
if (config->allow_daemon_threads) {
625+
interp->feature_flags |= Py_RTFLAGS_DAEMON_THREADS;
626+
}
624627
}
625628

626629

0 commit comments

Comments
 (0)