Pull requests / #1862
#1862 serve: Ctrl+C prints "Exception ignored in: <_io.TextIOWrapper ...>" from the vision encoder's buffered QUIT
open · @weituotian · 0 comentários · No GitHub
Server & APIModels & quantsWindowsLinux
Descrição
Stopping the server with Ctrl+C, with an image encoder configured (`"vision"` in the config), prints two lines of Python noise between `[strata] stopping` and `[strata] stopped`:
```
[strata] stopping (Ctrl+C again to end the engine at once) ...
Exception ignored in: <_io.TextIOWrapper name=4 encoding='utf-8'>
Traceback (most recent call last):
File "D:\projects\ai\Strata\serve\server.py", line 2076, in shutdown
self.close()
OSError: [Errno 22] Invalid argument
[strata] stopped
```
The model stops correctly and the shell prompt comes back - the shutdown itself is fine, it is only the report. But it names a line of our own code at every stop, which reads like a crash.
### Why it happens
`Vision.close()` writes `QUIT` into the encoder's stdin and then drops the process:
```python
try:
proc.stdin.write("QUIT\n")
proc.stdin.flush()
proc.wait(timeout=10)
except Exception:
...
self.stopped = True # proc, and its pipes, are left to the GC
```
The pipes are made with `text=True, bufsize=1`, so the newline flushes inside `write()`. Ctrl+C reaches every process attached to the console, so the encoder can already be gone by then, and that flush raises `OSError [Errno 22]` - the existing `except Exception` catches it, so the shutdown carries on. But `QUIT` is then still in the `TextIOWrapper`'s buffer. When `proc` goes out of scope at the end of `close()`, the wrapper's finalizer flushes it again, raises the same `OSError`, and CPython prints it as `Exception ignored in: <_io.TextIOWrapper name=4 ...>`, with the caller's frame (`shutdown`, the `self.close()` line) - exactly the two lines above.
`StrataEngine.close()` has never shown this, because it already closes its stdin and ignores the error, with a comment that names this very case:
```python
self.proc.stdin.close() # buffered data may flush again after the engine has exited
```
(serve/server.py:1863). The encoder's `close()` never got the same guard. That is the whole of this PR.
### Minimal reproduction, no model and no GPU needed
Windows 11 26100, CPython 3.13.14:
```python
import gc, subprocess, sys
def arm(explicit_close):
p = subprocess.Popen([sys.executable, "-c", "pass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, encoding="utf-8", bufsize=1)
p.wait(timeout=10) # the child (the "encoder") is gone, its read end is closed
try:
p.stdin.write("QUIT\n")
p.stdin.flush()
except OSError as e:
print(" the flush raised:", e)
if explicit_close: # what this PR does
for pipe in (p.stdin, p.stdout):
try:
pipe.close()
except OSError:
pass
del p
gc.collect()
```
Arm A, as the code is today:
```
the flush raised: [Errno 22] Invalid argument
Exception ignored in: <_io.TextIOWrapper name=3 encoding='utf-8'>
Traceback (most recent call last):
File "pipe_gc_test.py", line 30, in arm
del p
OSError: [Errno 22] Invalid argument
```
Arm B, with the pipes closed here and the error ignored: the `close()` raises the same `[Errno 22]`, it is swallowed, the wrapper is marked closed, and the GC prints nothing at all.
### On a real server
1. Start it with an image encoder in the config (any model), wait for `ready:`.
2. Press Ctrl+C once.
3. The two lines appear between `[strata] stopping` and `[strata] stopped`; the fix removes them.
Without a `"vision"` section the lines never appear in the first place - only `vision.shutdown` is in the closer list, which is why this went unnoticed for so long.
### The change
Nine lines at the end of `Vision.close()` (serve/server.py:2164-2172): close the encoder's stdin there, ignoring `OSError`, the way `StrataEngine.close()` already does.
### What it leaves alone
- The shutdown sequence is untouched: the same `QUIT`, the same `wait(10)`, the same `kill`, the same `wait`.
- No new setting, no new default, no change on a healthy encoder: a successful `QUIT` leaves the buffer empty, so the added `close()` is a no-op there.
- One file, `serve/server.py`.
### How it was checked
- `python -m unittest discover -s serve -t . -p "test_*.py"` on this branch rebased onto `61b3fb5d`: **690 tests, OK (11 skipped)**.
- The reproduction above: arm A prints the message, arm B does not.
### Caveats
- Measured on Windows 11 26100, CPython 3.13.14, engine 0.1.41, encoder on the CPU (`"gpu": false`). **No Linux run.** The mechanism is not Windows-specific (SIGINT goes to the whole process group there too, and the second flush would raise `BrokenPipeError` instead), but I have not reproduced it on Linux and this PR does not claim it.
- I searched the issues and pull requests first: `TextIOWrapper` and `Exception ignored` find nothing, so this is a new thread rather than a reply to an existing one (#352 and #1317 are about the encoder's pipe in other ways, #181 about ending the processes with the server).
No site
Links install, modelos, releases.