honcho icon indicating copy to clipboard operation
honcho copied to clipboard

Printer / Manager doesn't handle BrokenPipeError on write if the output is piped

Open indrat opened this issue 6 months ago • 0 comments

Given a Procfile like:

app: env PORT=8000 PYTHONUNBUFFERED=1 uvicorn app:app

And the app.py of:

async def app(scope, receive, send):
    assert scope['type'] == 'http'

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            (b'content-type', b'text/plain'),
            (b'content-length', b'13'),
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

And honcho started in a terminal with something like:

pip install honcho uvicorn
honcho start | tee output.txt

And then killed with a CTRL-C or multiple CTRL-Cs depending on where in the loop that the KeyboardInterrupt is handled the Printer.write method will raise BrokenPipeError that isn't handled and honcho will exit potentially leaving the child processes still running and subsequent restarts fail due to the address already being bound.

› honcho start | tee output.txt
13:51:35 system | app.1 started (pid=13467)

13:51:35 app.1  | INFO:     Started server process [13467]
13:51:35 app.1  | INFO:     Waiting for application startup.
13:51:35 app.1  | INFO:     ASGI 'lifespan' protocol appears unsupported.
13:51:35 app.1  | INFO:     Application startup complete.
13:51:35 app.1  | INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)


^CTraceback (most recent call last):
  File "/projects/honcho/venv/bin/honcho", line 8, in <module>
    sys.exit(main())
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/command.py", line 318, in main
    COMMANDS[args.command](args)
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/command.py", line 254, in command_start
    manager.loop()
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/manager.py", line 110, in loop
    msg = self.events.get(timeout=0.1)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/multiprocessing/queues.py", line 113, in get
    if not self._poll(timeout):
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/multiprocessing/connection.py", line 262, in poll
    return self._poll(timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/multiprocessing/connection.py", line 429, in _poll
    r = wait([self], timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/multiprocessing/connection.py", line 936, in wait
    ready = selector.select(timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/selectors.py", line 416, in select
    fd_event_list = self._selector.poll(timeout)
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/manager.py", line 98, in _terminate
    self.terminate()
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/manager.py", line 149, in terminate
    self._killall()
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/manager.py", line 168, in _killall
    self._system_print("sending %s to %s (pid %s)\n" %
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/manager.py", line 192, in _system_print
    self._printer.write(Message(type='line',
  File "/projects/honcho/venv/lib/python3.9/site-packages/honcho/printer.py", line 61, in write
    print(prefix + line, file=self.output, flush=True)
BrokenPipeError: [Errno 32] Broken pipe


^CException ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

› honcho start | tee output.txt
13:51:43 system | app.1 started (pid=13487)
13:51:43 app.1  | INFO:     Started server process [13487]
13:51:43 app.1  | INFO:     Waiting for application startup.
13:51:43 app.1  | INFO:     ASGI 'lifespan' protocol appears unsupported.
13:51:43 app.1  | INFO:     Application startup complete.
13:51:43 app.1  | ERROR:    [Errno 48] error while attempting to bind on address ('127.0.0.1', 8000): address already in use
13:51:43 system | app.1 stopped (rc=1)

Per the python docs the best approach is probably to drop any future messages. Wrapping the call to print in Printer.write with a try..except BrokenPipeError per the note about SIGPIPE does allow honcho to handle the broken pipe and clean up child processes successfully, however there's probably a better way or at least requires a check for whether a BrokenPipeError has already been handled or not.

            try:
                print(prefix + line, file=self.output, flush=True)
            except BrokenPipeError:
                # Python flushes standard streams on exit; redirect remaining output
                # to devnull to avoid another BrokenPipeError at shutdown
                devnull = os.open(os.devnull, os.O_WRONLY)
                os.dup2(devnull, self.output.fileno())

indrat avatar Jun 11 '25 04:06 indrat