Home > Programming > Asynchronous Shell Commands with Tornado

Asynchronous Shell Commands with Tornado

I’ve been playing around with Tornado a bit and wanted to asynchronously call a long-running shell command without blocking the server process. Here is my solution.

Running this server I am able to visit http://localhost:8888/test/ numerous times while a request for http://localhost:8888/ is waiting for the command to finish. The beauty of this is that this is a single process with a single thread. With a process this light and fast, a relatively large number of applications can be crammed into a $10/mo hosted account.

#!/usr/bin/env python
import os
import tornado.httpserver
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.ioloop = tornado.ioloop.IOLoop.instance()
        self.pipe = p = os.popen('sleep 5; cat /etc/mime.types')
        self.ioloop.add_handler( p.fileno(), self.async_callback(self.on_response), self.ioloop.READ )

    def on_response(self,fd,events):
        for line in self.pipe:
            self.write( line )

        self.ioloop.remove_handler(fd)
        self.finish()

class TestHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('this is a test')

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/test/", TestHandler),
])

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Categories: Programming Tags: , ,
  1. December 22, 2009 at 6:26 am | #1

    Nice solution. (You should maybe change to subprocess.Popen, as os.popen is deprecated in 2.6+)

  2. December 22, 2009 at 7:17 am | #2

    Yes, I was very disappointed that os.popen was deprecated. subprocess.Popen may be more flexible, but it’s also more complicated. For most cases I used it for os.popen was perfect. Now I just have to write more code every time I use it.

  3. December 29, 2009 at 10:36 pm | #3

    Thanks for this code! I was able to adapt it for use on my site.

  4. Ptg
    October 14, 2010 at 2:21 am | #4

    Is there an easy way to put a timeout on it? I.e. to run the command, but if it doesn’t complete in 30 seconds then terminate it anyway?

  5. October 14, 2010 at 3:57 am | #5

    I’m afraid I don’t know the answer to that question.

  6. Brian
    November 5, 2010 at 10:48 am | #6

    I’ve noticed that requests to the same handler seem to be handled synchronously. Try loading your MainHandler in three windows at the same time; the requests will finish around the 5, 10, and 15 second marks. I haven’t been able to conquer this problem, and I’m not sure if you know more about it than I do. Any thoughts?

  7. November 5, 2010 at 11:17 am | #7

    Brian,

    I’m quite certain this was working correctly at one point. But I did just try it again and it is doing what you said. Let me know if you figure out what’s happening.

  8. Brian
    November 5, 2010 at 11:22 am | #8

    I’ve reproduced the problem using multiprocessing.Process & Pipe intead of popen as well as using a completed form of the example given on the official Tornado documentation:

    http://www.tornadoweb.org/documentation#non-blocking-asynchronous-requests

    I’ve been asking questions on the Tornado mailing list lately, though this recent discovery has been unanswered so far.

  9. November 5, 2010 at 12:12 pm | #9

    Make sure you have epoll working. I did at one time, but it may have broken since. Check here:

    https://github.com/facebook/tornado/blob/master/tornado/ioloop.py#L36

  10. Brian
    November 9, 2010 at 12:25 pm | #10

    It was the browser! Try using two different browsers or two instances of cURL.

    This was answered by Ben Darnell in my e-mail to the Tornado mailing list:
    http://groups.google.com/group/python-tornado/browse_thread/thread/fa695c938d5092dc

  11. November 9, 2010 at 12:35 pm | #11

    Brian,

    Thanks for following up on that.

  12. Ben
    January 25, 2011 at 4:20 pm | #12

    This implementation worked for me using the “subprocess” module:
    —————————————————————–
    @tornado.web.asynchronous
    def post(self):
    #…
    #… get your inputs… like self.get_argument(‘whatever blah blah’)
    #…

    #spawn process, feed it, and then move on
    self.ioloop = tornado.ioloop.IOLoop.instance()
    self.sp = subprocess.Popen([command,args,pamplemousse],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE)

    #my application has a lot of input, too much for the command line
    #so we feed in the data via stdin. You may not need to do this part,
    #depending on your application
    self.sp.stdin.write(‘here is my input la la la’)

    #make the callback!
    self.ioloop.add_handler(self.sp.stdout.fileno(),
    self.async_callback(self.on_response),
    self.ioloop.READ)

    def on_response(self):
    response = self.sp.stdout.read()
    self.write(response)
    self.ioloop.remove_handler(fd)
    self.finish()

  13. November 16, 2011 at 10:11 am | #13

    I’ve created an updated version of a previous gist which can be found here: https://gist.github.com/1370533 and includes an example using the gen.engine yield way of doing things.

    Small explanation on my personal blog here: http://cerealkillers.co.uk/2011/async-shell-commands-from-tornado/

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.