Printer / Manager doesn't handle BrokenPipeError on write if the output is piped
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())