views:

391

answers:

3

I've got a problem with using the io_add_watch monitor in python (via gobject). I want to do a nonblocking read of the whole buffer after every notification. Here's the code (shortened a bit):

class SomeApp(object):

   def __init__(self):
      # some other init that does a lot of stderr debug writes
      fl = fcntl.fcntl(0, fcntl.F_GETFL, 0)
      fcntl.fcntl(0, fcntl.F_SETFL, fl | os.O_NONBLOCK)
      print "hooked", gobject.io_add_watch(0, gobject.IO_IN | gobject.IO_PRI, self.got_message, [""])
      self.app = gobject.MainLoop()

   def run(self):
      print "ready"
      self.app.run()

   def got_message(self, fd, condition, data):
      print "reading now"
      data[0] += os.read(0, 1024)
      print "got something", fd, condition, data
      return True

gobject.threads_init()
SomeApp().run()

Here's the trick - when I run the program without debug output activated, I don't get the got_message calls. When I write a lot of stuff to the stderr first, the problem disappears. If I don't write anything apart from the prints visible in this code, I don't get the stdin messsage signals. Another interesting thing is that when I try to run the same app with stderr debug enabled but via strace (to check if there are any fcntl / ioctl calls I missed), the problem appears again.

So in short: if I write a lot to stderr first without strace, io_watch works. If I write a lot with strace, or don't write at all io_watch doesn't work.

The "some other init" part takes some time, so if I type some text before I see "hooked 2" output and then press "ctrl+c" after "ready", the get_message callback is called, but the read call throws EAGAIN, so the buffer seems to be empty.

Strace log related to the stdin:

ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
fcntl(0, F_GETFL)                       = 0xa002 (flags O_RDWR|O_ASYNC|O_LARGEFILE)
fcntl(0, F_SETFL, O_RDWR|O_NONBLOCK|O_ASYNC|O_LARGEFILE) = 0
fcntl(0, F_GETFL)                       = 0xa802 (flags O_RDWR|O_NONBLOCK|O_ASYNC|O_LARGEFILE)

Does anyone have some ideas on what's going on here?


EDIT: Another clue. I tried to refactor the app to do the reading in a different thread and pass it back via a pipe. It "kind of" works:

...
      rpipe, wpipe = os.pipe()
      stopped = threading.Event()
      self.stdreader = threading.Thread(name = "reader", target = self.std_read_loop, args = (wpipe, stopped))
      self.stdreader.start()
      new_data = ""
      print "hooked", gobject.io_add_watch(rpipe, gobject.IO_IN | gobject.IO_PRI, self.got_message, [new_data])

   def std_read_loop(self, wpipe, stop_event):
      while True:
         try:
            new_data = os.read(0, 1024)
            while len(new_data) > 0:
               l = os.write(wpipe, new_data)
               new_data = new_data[l:]
         except OSError, e:
            if stop_event.isSet():
               break
            time.sleep(0.1)
...

It's surprising that if I just put the same text in a new pipe, everything starts to work. The problem is that:

  • the first line is not "noticed" at all - I get only the second and following lines
  • it's fugly

Maybe that will give someone else a clue on why that's happening?

A: 

The documentation says you should return TRUE from the callback or it will be removed from the list of event sources.

ntd
It's returning True in the real code. It doesn't matter though - if it's not called, it's not called at all - even once.
viraptor
+1  A: 

This sounds like a race condition in which there is some delay to setting your callback, or else there is a change in the environment which affects whether or not you can set the callback.

I would look carefully at what happens before you call io_add_watch(). For instance the Python fcntl docs say:

All functions in this module take a file descriptor fd as their first argument. This can be an integer file descriptor, such as returned by sys.stdin.fileno(), or a file object, such as sys.stdin itself, which provides a fileno() which returns a genuine file descriptor.

Clearly that is not what you are doing when you assume that STDIN will have FD == 0. I would change that first and try again.

The other thing is that if the FD is already blocked, then your process could be waiting while other non-blocked processes are running, therefore there is a timing difference depending on what you do first. What happens if you refactor the fcntl stuff so that it is done soon after the program starts, even before importing the GTK modules?

I'm not sure that I understand why a program using the GTK GUI would want to read from the standard input in the first place. If you are actually trying to capture the output of another process, you should use the subprocess module to set up a pipe, then io_add_watch() on the pipe like so:

proc = subprocess.Popen(command, stdout = subprocess.PIPE)
gobject.io_add_watch(proc.stdout, glib.IO_IN, self.write_to_buffer )

Again, in this example we make sure that we have a valid opened FD before calling io_add_watch().

Normally, when gobject.io_add_watch() is used, it is called just before gobject.MainLoop(). For example, here is some working code using io_add_watch to catch IO_IN.

Michael Dillon
The descriptor I need is guaranteed to be 0 in this case. I need to handle it as a descriptor / it's not a subprocess. Moving fnctl didn't work unfortunately. I'm not using gtk, just gobject / gst integration. But good suggestions anyways. Unfortunately my code still doesn't work.
viraptor
I'm not sure why you are throwing threads into the mix because that just increases complexity and creates more opportunities for race conditions. Have a look at the snippets here, particularly the one that implements a player with GStreamer http://pyneo.org/documentation/snippets/
Michael Dillon
Here is another example of a GStreamer player http://de.pastebin.ca/raw/933910 I really think that you need to simplify this down to something that works, and build up from there, rather than the other way around.
Michael Dillon
A: 

What happens if you hook the callback first, prior to any stderr output? Does it still get called when you have debug output enabled?

Also, I suppose you should probably be repeatedly calling os.read() in your handler until it gives no data, in case >1024 bytes become ready between calls.

Have you tried using the select module in a background thread to emulate gio functionality? Does that work? What platform is this and what kind of FD are you dealing with? (file? socket? pipe?)

Walter Mundt
Changing the order of initialisation didn't help. I tried flushing the descriptors first (with both flush and read) and that didn't help. I'm running that on linux and the fd is a pipe opened by a program that spawns my app. It works with threading + blocking reads in the background, but I'm already using glib's io for other fds in the same program, so I don't want to use a different method for every input.
viraptor