Pull requests / #1862

#1862 serve: Ctrl+C prints "Exception ignored in: <_io.TextIOWrapper ...>" from the vision encoder's buffered QUIT

open · @weituotian · 0 comentarios · En GitHub

Server & APIModels & quantsWindowsLinux

Descripción

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).

En el sitio

Enlaces a install, modelos, releases.