tkinter: os.dup2() on fds 1/2 breaks every subsequent Tcl interpreter on Windows
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 5/5
- Geschätzter Aufwand
- Über eine Woche
- Anfängerfreundlichkeit
- 38/100
Rechercherichtung
Start with Modules/_tkinter.c and the Tcl ticket linked in the issue; trace how Tkapp_New() initializes Tcl and its standard channels, then reproduce the Windows failure with the provided script. The issue proposes installing channels from duplicated handles via Tcl_SetStdChannel(), but notes that this direction has not been validated in C. Done means subsequent Tk() and Tcl() interpreters initialize successfully after fd 1 and 2 are redirected and restored.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Bug report
Bug description:
On Windows, once a Tcl interpreter has built its standard channels, a later os.dup2() onto fds 1 and 2 can make every subsequent tkinter.Tk() or tkinter.Tcl() fail:
_tkinter.TclError: Can't find a usable init.tcl in the following directories:
{C:\Users\...\Python314\tcl\tcl8.6}
C:/Users/.../tcl/tcl8.6/init.tcl: couldn't read file "C:/Users/.../tcl/tcl8.6/init.tcl": no such file or directory
The file is present and readable the whole time. At the instant of the failure Python can os.stat() and open() that same path, and a Tcl interpreter created before the redirection can still source it. The errno is not real.
This is not GUI-specific — tkinter.Tcl() is affected too — and it does not need threads, subprocesses or a window.
Reproducer
Fails 10/10 runs here, with stdout going to a console, a pipe, a file or NUL, for both Tk() and Tcl():
import os, sys, tempfile, tkinter
saved = {fd: os.dup(fd) for fd in (1, 2)}
tmp = {fd: tempfile.TemporaryFile() for fd in (1, 2)}
use_tcl = "--tcl" in sys.argv
def new_interpreter():
if use_tcl:
ip = tkinter.Tcl() # no GUI at all
ip.eval("fconfigure stdout") # make Tcl build its std channels
del ip
else:
tkinter.Tk().destroy()
for i in range(30):
for fd in (1, 2): # redirect fds 1 and 2 ...
os.dup2(tmp[fd].fileno(), fd)
try:
new_interpreter()
except Exception as exc:
for fd in (1, 2):
os.dup2(saved[fd], fd)
print(f"interpreter {i} failed:\n{exc}")
sys.exit(1)
for fd in (1, 2): # ... and put them back
os.dup2(saved[fd], fd)
print("30 interpreters, no failure")
It usually fails at interpreter 1 or 2. Two interpreters and two dup2() calls are enough to trigger it, but that minimal form is sensitive to how handle values happen to be laid out — it fails 18/20 when the real stdout is NUL and 0/20 when it is a pipe. The loop above is stable everywhere, so it is the better reproducer.
What is happening
- Tcl builds its
stdout/stderrchannels on first use out of the valuesGetStdHandle()returns, and keeps them for the life of the process. os.dup2()on fds 1 and 2 goes through the UCRT's_dup2, which for fds 0/1/2 closes the handle currently installed and publishes the new one withSetStdHandle(). The values Tcl cached are now closed handles.- Windows recycles those values immediately. Traced across one redirect/restore cycle (
#fd:handle/type):
start fd1=#1:184/PIPE fd2=#2:188/PIPE stdout=184 stderr=188
after redirect fd1=#1:700/DISK fd2=#2:184/DISK stdout=700 stderr=184
after restore fd1=#1:724/PIPE fd2=#2:700/PIPE stdout=724 stderr=700
Handle 184 is the real stdout pipe at startup and a temporary file one dup2 later. Handle 700 is what Tcl cached as stdout for the first interpreter, and one dup2 later it belongs to fd 2.
- Creating a further Tcl interpreter then fails while reading its own
init.tcl.
Step 1 is the necessary condition, and it is testable on its own. A bare Tcl interpreter that never touches its standard channels never fails; add one fconfigure and it fails like the rest:
60 interpreters per run, same dup2 pattern |
runs failed |
|---|---|
tkinter.Tcl(), standard channels never used |
0/10 |
same, plus fconfigure stdout; fconfigure stderr |
10/10 |
Tk matters only because Tk_Init creates those channels.
The converse holds too. If the handles Tcl caches are private duplicates that nothing else can close, the failure disappears. Done from Python with ctypes — DuplicateHandle() the current fd 1/2 handles, SetStdHandle() the copies, create one interpreter so Tcl caches them, then never close them:
| runs failed | |
|---|---|
| as above | 15/15 |
| identical, but std handles pinned to private duplicates first | 0/15 |
Why this looks like a _tkinter issue
This is Tcl ticket 91c9bc1c45, "Standard channels (stdin, stdout, stderr) on Windows may get broken by dup2()", filed 2018-11-09 against Tcl 8.6.8. It names the same mechanism — TclpGetDefaultStdChannel() in win/tclWinChan.c, broken by _dup2 on fds 0/1/2 — and proposes DuplicateHandle() on the value from GetStdHandle() before handing it to Tcl_MakeFileChannel().
It was closed Invalid on 2026-10-15, with the stated reason that there had been no reply since 2018. The 2018 objection from a Tcl maintainer was that third-party code should call Tcl_SetStdChannel() when it changes the standard handles. In this situation _tkinter is the only component in a position to do that: os.dup2() on fds 0/1/2 is ordinary, supported Python, and the code calling it has no idea Tcl is embedded in the process.
So the equivalent of the upstream patch, on this side, would be for _tkinter to install the standard channels itself from duplicated handles via Tcl_SetStdChannel() — once per process, before Tcl_Init() runs in Tkapp_New() (Modules/_tkinter.c). I have only verified the ctypes equivalent above, not a C patch, so I may well be missing a constraint; I am happy to put a patch together if this direction seems right.
Why it matters in practice
pytest's default --capture=fd does exactly this dup2 pattern around every test. Any Windows test suite that creates a Tcl/Tk interpreter per test hits it:
pytest configuration, 30 tests creating one Tk() each |
runs failed |
|---|---|
--capture=fd (the default) |
16/20 |
-s / --capture=sys / --capture=tee-sys |
0/20 |
one test creating 30 interpreters in a loop, --capture=fd |
0/20 |
That is pytest-dev/pytest#10364, open since 2022. Applying the pin described above from a conftest.py takes that suite from 10/20 runs failing to 0/20.
What I have not established
I have not traced the step from "Tcl is holding a stale standard handle" to the specific couldn't read file ...: no such file or directory. The reported errno is clearly not the real one, and the upstream ticket describes a different surface symptom from the same cause (failing writes, EBADF) — so I assume it is stale rather than meaningful, but I have not proved it.
Two things that did not pan out, in case they save someone time: Tcl does not close standard handles it does not own (a planted sentinel handle survives interpreter create + destroy), and creating a Tk root before the redirection does not help, because the first dup2 closes the original stdout handle too.
CPython versions tested on:
3.14 (3.14.4, Windows installer build, Tcl/Tk 8.6.15). I do not have other versions on this machine, so I have not checked whether main behaves differently — nothing in the mechanism looks version-specific.
Operating systems tested on:
Windows (Windows 11 Pro, build 26200).
- Vorherrschende Sprache
- Python
- Sterne
- 78.2k
- Forks
- 36.6k
- Ø Merge
- 19 Std. 57 Min.
- Gemergte PRs (30 T.)
- 630
Entwicklungsumgebung
Startet den Dev-Container des Projekts im Browser, mit Ihrem eigenen GitHub-Konto.
- Kein Dockerfile und keine Docker-Compose-Datei
- Hat eine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus python/cpython
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
Maintainer antworten meist innerhalb von 1 Tag
-
stdlib type-bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
Maintainer antworten meist innerhalb von 1 Tag
-
extension-modules stdlib topic-XML type-bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
python/cpython#161807 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
stdlib type-feature
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
python/cpython#161788 · 7 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
extension-modules topic-IO type-bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
Maintainer antworten meist innerhalb von 1 Tag
Ähnliche Issues
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 62/100
cpp-best-practices/project_validator#267 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
Project-N-E-K-O/N.E.K.O#3725 ·
Maintainer antworten meist innerhalb von 1 Tag
-
[Bug] Sending a Telegram message chain twice duplicates mentionsEvtl. vergeben @123XieC hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
AstrBotDevs/AstrBot#11125 ·
Maintainer antworten meist innerhalb von 1 Tag
-
backend bug priority: high
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
nooriegarg/ContribIQ#73 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug p3 security
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
amd/gaia#8197 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag