Skip to content

Commit a33d31c

Browse files
committed
Correct the watchdog poll mask note and use the public get_osfhandle
The null device reports POLLIN (plus POLLOUT under the default event mask), never POLLERR; the migration note taught the wrong mask. The Windows handle rebind now uses msvcrt.get_osfhandle, the documented public API, instead of pywin32's underscore-private equivalent, which also drops the win32file import. The migration entry also gains the one API-visible change: a second concurrent stdio_server() raises RuntimeError.
1 parent cbac04d commit a33d31c

2 files changed

Lines changed: 15 additions & 9 deletions

File tree

docs/migration.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,15 +1866,20 @@ during a session now sees the diversions, not the wire.
18661866

18671867
One pattern needs migrating: watchdog threads that watch fd 0 to detect a vanished
18681868
client (a POSIX-specific pattern; `select.poll` does not exist on Windows). The null
1869-
device does not behave like the old pipe: it never reports `POLLHUP`, and it reports
1870-
readable immediately and permanently (`POLLIN|POLLERR` from `poll()` on Linux, ready
1871-
from `select()`, and macOS can report `POLLNVAL` for devices). A watcher waiting for
1872-
`POLLHUP` is silently disarmed; a watcher that treats any event as "client gone" now
1873-
fires at startup instead of never. Watch the parent process instead: on POSIX, exit
1869+
device does not behave like the old pipe: it never reports `POLLHUP` or `POLLERR`,
1870+
and it reports readable immediately and permanently (`POLLIN` from `poll()` on Linux,
1871+
plus `POLLOUT` under the default event mask; ready from `select()`; and macOS can
1872+
report `POLLNVAL` for devices). A watcher waiting for `POLLHUP` or `POLLERR` is
1873+
silently disarmed; a watcher that treats any event as "client gone" now fires at
1874+
startup instead of never. Watch the parent process instead: on POSIX, exit
18741875
when `os.getppid()` changes, which happens when the client dies because orphaned
18751876
processes are reparented. That works on both v1 and v2 and does not depend on
18761877
descriptor layout.
18771878

1879+
Also new: a second concurrent `stdio_server()` on the process's default streams now
1880+
raises `RuntimeError` instead of silently contending for stdin, a configuration that
1881+
never worked (there is one stdin).
1882+
18781883
Also worth knowing: a child process that streams large output to its inherited
18791884
stdout now streams it into the client's stderr channel. Capture output you do not
18801885
want in the client's logs, and be aware that a client which never drains its stderr

src/mcp/os/win32/utilities.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717

1818
# Windows-specific imports for Job Objects
1919
if sys.platform == "win32":
20+
import msvcrt
21+
2022
import pywintypes
2123
import win32api
2224
import win32con
23-
import win32file
2425
import win32job
2526
else:
2627
# Type stubs for non-Windows platforms
2728
win32api = None
2829
win32con = None
29-
win32file = None
30+
msvcrt = None
3031
win32job = None
3132
pywintypes = None
3233

@@ -40,11 +41,11 @@ def rebind_std_handle_to_fd(fd: int) -> None:
4041
Raises:
4142
OSError: The slot could not be set.
4243
"""
43-
if sys.platform != "win32" or not win32api or not win32file or not pywintypes:
44+
if sys.platform != "win32" or not win32api or not msvcrt or not pywintypes:
4445
return
4546
std_ids = {0: win32api.STD_INPUT_HANDLE, 1: win32api.STD_OUTPUT_HANDLE, 2: win32api.STD_ERROR_HANDLE}
4647
try:
47-
win32api.SetStdHandle(std_ids[fd], win32file._get_osfhandle(fd))
48+
win32api.SetStdHandle(std_ids[fd], msvcrt.get_osfhandle(fd))
4849
except pywintypes.error as exc:
4950
# Normalized so callers' OSError-based best-effort handling covers it.
5051
raise OSError(f"SetStdHandle failed for fd {fd}") from exc

0 commit comments

Comments
 (0)