Following this post, I am able to tail -f
a log file to a webpage:
from gevent import sleep
from gevent.wsgi import WSGIServer
import flask
import subprocess
app = flask.Flask(__name__)
@app.route('/yield')
def index():
def inner():
proc = subprocess.Popen(
['tail -f ./log'],
shell=True,
stdout=subprocess.PIPE
)
for line in iter(proc.stdout.readline,''):
sleep(0.1)
yield line.rstrip() + '<br/>\n'
return flask.Response(inner(), mimetype='text/html')
http_server = WSGIServer(('', 5000), app)
http_server.serve_forever()
There are two issues in this approach.
- The
tail -f log
process will linger after closing the webpage. There will be n tail process after visiting http://localhost:5000/yield n time
- There can only be 1 client accessing http://localhost:5000/yield at a single time
My question(s) is, is it possible to make flask execute a shell command when someone visit a page and terminating the command when client close the page? Like Ctrl+C after tail -f log
. If not, what are the alternatives?
Why was I only able to have 1 client accessing the page at a time?
Note: I am looking into general way of starting/stoping an arbitrary shell command instead of particularly tailing a file
Here is some code that should do the job. Some notes:
You need to detect when the request disconnects, and then terminate the proc. The try/except code below will do that. However, after inner() reaches its end, Python will try to close the socket normally, which will raise an exception (I think it's socket.error, per How to handle a broken pipe (SIGPIPE) in python?). I can't find a way to catch this exception cleanly; e.g., it doesn't work if I explicitly raise StopIteration at the end of inner(), and surround that with a try/except socket.error block. That may be a limitation of Python's exception handling. There may be something else you can do within the generator function to tell flask to abort streaming without trying to close the socket normally, but I haven't found it.
Your main thread is blocking during proc.stdout.readline(), and gevent.sleep() comes too late to help. In principle gevent.monkey.patch_all() can patch the standard library so that functions that would normally block the thread will yield control to gevent instead (see http://www.gevent.org/gevent.monkey.html). However, that doesn't seem to patch proc.stdout.readline(). The code below uses gevent.select.select() to wait for data to become available on proc.stdout or proc.stderr before yielding the new data. This allows gevent to run other greenlets (e.g., serve other web clients) while waiting.
The webserver seems to buffer the first few kB of data being sent to the client, so you may not see anything in your web browser until a number of new lines have been added to ./log. After that, it seems to send new data immediately. Not sure how to get the first part of the request to be sent right away, but it's probably a pretty common problem with streaming servers, so there should be a solution. This isn't a problem with commands that terminate quickly on their own, since their full output will be sent once they terminate.
You may also find something useful at https://mortoray.com/2014/03/04/http-streaming-of-command-output-in-python-flask/ .
Here's the code:
from gevent.select import select
from gevent.wsgi import WSGIServer
import flask
import subprocess
app = flask.Flask(__name__)
@app.route('/yield')
def index():
def inner():
proc = subprocess.Popen(
['tail -f ./log'],
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# pass data until client disconnects, then terminate
# see https://stackoverflow.com/questions/18511119/stop-processing-flask-route-if-request-aborted
try:
awaiting = [proc.stdout, proc.stderr]
while awaiting:
# wait for output on one or more pipes, or for proc to close a pipe
ready, _, _ = select(awaiting, [], [])
for pipe in ready:
line = pipe.readline()
if line:
# some output to report
print "sending line:", line.replace('\n', '\\n')
yield line.rstrip() + '<br/>\n'
else:
# EOF, pipe was closed by proc
awaiting.remove(pipe)
if proc.poll() is None:
print "process closed stdout and stderr but didn't terminate; terminating now."
proc.terminate()
except GeneratorExit:
# occurs when new output is yielded to a disconnected client
print 'client disconnected, killing process'
proc.terminate()
# wait for proc to finish and get return code
ret_code = proc.wait()
print "process return code:", ret_code
return flask.Response(inner(), mimetype='text/html')
http_server = WSGIServer(('', 5000), app)
http_server.serve_forever()