Sunday, January 10, 2010

Go, syscall, and inotify

At the time of writing this article, Go does not implement all of the syscalls for which it has constants.  One of these is inotify, which allows one to monitor file system events.  It, and similar libraries are what allow file managers such as KDE's Dolphin or Gnome's Nautilus to almost immediately show a file's new date-stamp when it has been modified.

For information on inotify, check out the following article at LinuxJournal.com:

http://www.linuxjournal.com/article/8478

Fortunately, Go's syscall package exports the low-level function that it uses for all implemented syscalls.  This function is named, appropriately, Syscall.  There is also Syscall6, which receives six arguments (in addition to the syscall id) instead of three.  For our purposes, Syscall will be sufficient.

First, the basics:

Syscall receives and returns all values as the type uintptr.  This seems strange and at a glance would seem to indicate that it deals with pointers to all values, but that is actually not necessarily the case.  Integers, for example, are simply converted directly via a cast.

param := uintptr (num)

Strings, however, are a slightly more complex affair, but the syscall package provides a necessary function, StringBytePtr, which simply converts the string to a byte slice and returns a pointer to the first element.

param := uintptr (unsafe.Pointer (StringBytePtr (str)))

Although Syscall must receive three values (in addition to the syscall id), if the particular syscall requires less, one simply passes 0 to the remainder.  Similarily, it always returns three values, though not all are meaningful.  All are of the type uintptr: the first two being the actual return values, while the last is an error code.  Values that are not needed may be received as an empty variable: _.

The os package contains the functionality needed to print syscall error codes.  os.NewSyscallError receives a string and an integer.  The first should be the name of the syscall you attempted, while the last is the error code that Syscall returned (casted to int, of course).  The structure returned implements os.Error.

In C inotify consists of three functions: inotify_init, inotify_add_watch, and inotify_rm_watch.  The corresponding syscall ids are SYS_INOTIFY_INIT, SYS_INOTIFY_ADD_WATCH, and SYS_INOTIFY_RM_WATCH.  Given the above information, it is simple to call each.  For example:

fd , _, errptr = syscall.Syscall (syscall.SYS_INOTIFY_INIT, 0, 0, 0);
if err = os.NewSyscallError ("SYS_INOTIFY_INIT", int (errptr)); err != nil {
    // deal with the error
}

The first returned variable fd is the file descriptor, the second is ignored, and the last is the error code.

pth := "/home/ostsol/Programming/Go/notify/blah.txt"
str := uintptr (unsafe.Pointer (syscall.StringBytePtr (pth)));
wd, _, errptr = syscall.Syscall (syscall.SYS_INOTIFY_ADD_WATCH,
uintptr (fd), str, 0x00000002);
if err = os.NewSyscallError ("SYS_INOTIFY_ADD_WATCH", int (errptr)); err != nil {
    //deal with the error
}

Unfortunately, Go does not expose the event masks for inotify.  0x00000002 is the mask for the modify event.

The event can be read via syscall.Read, which receives the file descriptor (casted to int) and an empty byte buffer.  The inotify event structure is, of course, not implemented in Go, so one has to create one's own:

type NotifyEvent struct {
    Wd int32;
    Mask uint32;
    Cookie uint32;
    Len uint32
}

Decoding the buffer filled by syscall.Read is accomplished via the encoding/binary package.  The binary.Read function expects a type that fulfills the io.Reader interface, which one can create via the NewReader function in the strings package.

The original inotify_event structure exposed via the C header sys/inotify.h also contains a name field.  You'll notice that its type is char[0].  We did not add this field to our structure because binary.Read would try to find a value to put in it.  The value only exists if the Len field is greater than zero.  If it is, we can extract the string from the byte buffer manually.

chars := make ([]byte, 80);
ln, errno := syscall.Read (fd, chars);
if err = os.NewSyscallError ("SYS_READ", errno); err != nil {
    // deal with it
}

event := new (NotifyEvent);

reader := strings.NewReader (string (chars[0:]));
if err = binary.Read (reader, binary.LittleEndian, event); err != nil {
    // deal with it
}

fmt.Println ("Event:");
fmt.Printf ("\twd: %d\n\tmask: %d\n\tcookie: %d\n\tlen %d\n",
event.Wd, event.Mask >> 16, event.Cookie, event.Len);

if event.Len > 0 {
   fmt.Printf ("\tname: %s", string (chars[16:16+event.Len]))
}

In a real system, multiple events may be returned by syscall.Read.  Thus, the buffer may have to be much larger and one will have to iterate through it, each time creating a reader further along in the buffer.

event := new (NotifyEvent);

for i := uint32 (0); i < uint32 (ln); i += 16 + event.Len {
reader := strings.NewReader (string (chars[i:]));
 // ...
}

There are two problems that I encountered in the creation of this article.  First is the event.Mask field.  The value it returns is 32768, which is not the flag that I input via SYS_NOTIFY_ADD_WATCH.  Secondly, using SYS_NOTIFY_RM_WATCH tosses me an 'invalid argument' error.  Thinking it was a Go problem, I re-wrote the program in C, but the same issues occurred.  It's strange, but everything else I did works solidly.  Hopefully this article helps despite the issues.

Followers