Learning Go (and integrating with inotify, too)

Go provides a native interface into the Linux inotify system. What’s inotify?  According to the man page, the inotify API provides a mechanism for monitoring file system events.  It can be used to monitor individual files, or directories.  There are a series of system calls available providing access to inotify. Here’s a quick run down of what they are and what they do.

All in all, it’s a fairly straightforward API.  Watches are configured with bitmasks and events are reported by feeding event structures to the read file descriptor.  For more information, see the Wikipedia page.

Where is it used? Well,  consider desktop indexing applications. For a lower level reference, the udevd system monitors udev rules via inotify and automatically reloads them when changes are detected.

Our Example Application

We’re not going to write anything as fancy as udevd, or as useful as a desktop indexing daemon.  Instead, we’ll put together a simple Go application that uses inotify integration and a collection of channels to calculate the size of files written to a directory. Nothing fancy.

What we Stick at the Top of the File

First thing’s first. We simply name our package and import others that we depend on.

package main import ("flag" "fmt" "os" "os/inotify" ) var watch_target *string = flag.String("watch", ".", "The directory to keep an eye on")

The only other thing going on here worthy of mention is the flag.String line. Since we want our tremendously useful application to be configurable, we’re using the flag module to set up a string flag.  This line sets up command line argument processing. The watch_target variable will contain the argument value as set by ‘–watch’, or the default, once control is passed to main(). The Go runtime will also automatically setup the “–help” flag as well, which will print the help strings for all arguments defined. Neat.

Our Changed File Structure

When we fire up our application, we want to read change events, pass them to a goroutine, and then simply forget about it.  When we pass the changes off, we need to tell our system which file changed as well as what to do with our result output.  We define the following structure.

type ChangedFile struct {
  output_channel chan string
  filename string
}

This simply contains two elements. An output channel that we’ll stream our results to and the filename of the changed entity.

Reporting Output

Next, we define a function that handles printing output to the screen. We run this in it’s own goroutine to keep concurrent routines from stepping on each other.

func notificationReporter(input chan string) {
  for {
    queue_data := <-input
    if len(queue_data) == 0 {
      return
    }
    fmt.Println("Change Received: ", queue_data)
  }
}

This is fairly straightforward. We pass in the channel in which we’ll read our input from. Next, we enter into an infinite loop. We just print the data to standard out.  However, if we get a zero-length string, we terminate the routine.  Though this isn’t really going to happen in our little example as we’re not rigging up sentinel values. We just quit on a Control+C.

The Change Processor

Next, we put together the method that handles processing of each individual change. It’s a method because it has a specific receiver, without it, we’d call it a function.  Here we stat the file changed, build a log string, and send that value to the channel attribute of the receiver.

func (c *ChangedFile)ProcessChange() {
  statbuf, _ := os.Stat(c.filename)
  c.output_channel <- fmt.Sprintf("%s is %d bytes", c.filename, statbuf.Size)
}

As you may have guessed by now, c.output_channel is the other end of the channel that we read from in the above notificationReporter function.  We’ll wire it all up in our main driver method.

The Main Method

Let’s just take a look at this one and then step through it afterwards.

func main() {
  flag.Parse()
  var report_channel = make(chan string, 5)
  go notificationReporter(report_channel)
  watcher, _ := inotify.NewWatcher()
  watcher.AddWatch(*watch_target, inotify.IN_CLOSE_WRITE)
  for {
    select {
      case event := <-watcher.Event:
        change := &ChangedFile{report_channel, event.Name}
        go change.ProcessChange()
      case error := <-watcher.Error:
        panic(error)
    }
  }
}

First off, we create a channel named report_channel, with a buffer of 5. This allows us to queue up to 5 results for writing before the enqueuing would block on a full “pipe.” Next, we fire off the notificationReporter function defined above. By using the go keyword, we allow it to run concurrently with our main line of control.

Next up, our inotify.Watcher is created. We then “load” the watcher with an event.  This takes two arguments – the directory we want to watch and the events we care about.  In this example, we just care about file close events for files that were written.

The for loop is slightly interesting.  Here you’ll see a Go select block. The select system is much like a switch statement, however, the individual cases are channels or IO endpoints.  If the tested object is ready to perform IO, then the following block will execute.  A default block could also be created, which would fire if none of the specific cases match. Finally, the select group will block until it is explicitly exited.

Within this loop, we create a new ChangedFile and configure it with our reporting channel and the name of the file that has been modified. We then execute change.ProcessChange by using the go keyword again, which causes it to run concurrently with the main line of control and the reporting function. We now have three concurrent paths executing at the same time.

Lastly, we just panic on error instead of building proper error handling in.

The Output

Alright, we’ll first run our code using the little build script we stuck in our ~/.profile file in the first Go blog entry.

[jeff@martian ~]$ gogogo test.go

There’s not a whole lot going on there. If everything works correctly, the code will compile and execute. From there, it will just block waiting for directory change events.  In another window, we’ll write a few files.

[jeff@martian ~]$ touch a b c d e; echo "bytes" > f

Alright, now if we switch back to the other window, we’ll see something interesting.

Change Received:  ./a is 0 bytes
Change Received:  ./b is 0 bytes
Change Received:  ./c is 0 bytes
Change Received:  ./d is 0 bytes
Change Received:  ./e is 0 bytes
Change Received:  ./f is 6 bytes

Well, that’s it. As each file is written and closed, our events are handled as outlined above.  When the fun is over, simply terminate the Go application with a Control+C.  As I learn a bit more about Go, I’ll get to closing down channels properly and cover that in a later post. Additionally, we didn’t close down our inotify handle correctly, either.  We rely on the operating system to handle all of that for us when the process terminates.

Posted on February 16, 2011 at 11:58 pm by Jeff McNeil · Permalink
In: development, Go, linux, open source

One Response

Subscribe to comments via RSS

  1. Written by GoDev.Ru » Blog Archive » Неделя Go
    on February 20, 2011 at 8:18 am
    Permalink

    [...] Learning Go (and integrating with inotify, too) Me at FOSDEM 2011: Practical Go Programming – nf.id.au Understanding Go compiler tools (1) – 世界線航跡蔵 Related Posts Put your related posts code here [...]

Subscribe to comments via RSS