server: Use a single inotify watch, as it scales better with a large
number of directories.
diff --git a/server/change.c b/server/change.c
index e32997e..aa8e0cb 100644
--- a/server/change.c
+++ b/server/change.c
@@ -28,6 +28,9 @@
 #include <stdlib.h>
 #include <signal.h>
 #include <sys/stat.h>
+#include <sys/types.h>
+#include <limits.h>
+#include <dirent.h>
 #include <errno.h>
 #ifdef HAVE_SYS_ERRNO_H
 #include <sys/errno.h>
@@ -126,6 +129,12 @@
 
 #endif
 
+struct inode;
+
+static void free_inode( struct inode *inode );
+
+static struct fd *inotify_fd;
+
 struct change_record {
     struct list entry;
     int action;
@@ -143,10 +152,10 @@
     int            notified; /* SIGIO counter */
     int            want_data; /* return change data */
     long           signaled; /* the file changed */
-    struct fd     *inotify_fd; /* inotify file descriptor */
-    int            wd;       /* inotify watch descriptor */
     struct list    change_q; /* change readers */
     struct list    change_records;   /* data for the change */
+    struct list    in_entry; /* entry in the inode dirs list */
+    struct inode  *inode;    /* inode of the associated directory */
 };
 
 static struct fd *dir_get_fd( struct object *obj );
@@ -241,30 +250,6 @@
     sigprocmask( SIG_UNBLOCK, &sigset, NULL );
 }
 
-struct object *create_dir_obj( struct fd *fd )
-{
-    struct dir *dir;
-
-    dir = alloc_object( &dir_ops );
-    if (!dir)
-        return NULL;
-
-    list_init( &dir->change_q );
-    list_init( &dir->change_records );
-    dir->event = NULL;
-    dir->filter = 0;
-    dir->notified = 0;
-    dir->signaled = 0;
-    dir->want_data = 0;
-    dir->inotify_fd = NULL;
-    dir->wd = -1;
-    grab_object( fd );
-    dir->fd = fd;
-    set_fd_user( fd, &dir_fd_ops, &dir->obj );
-
-    return &dir->obj;
-}
-
 static void dir_dump( struct object *obj, int verbose )
 {
     struct dir *dir = (struct dir *)obj;
@@ -352,8 +337,11 @@
     if (dir->filter)
         remove_change( dir );
 
-    if (dir->inotify_fd)
-        release_object( dir->inotify_fd );
+    if (dir->inode)
+    {
+        list_remove( &dir->in_entry );
+        free_inode( dir->inode );
+    }
 
     async_terminate_queue( &dir->change_q, STATUS_CANCELLED );
     while ((record = get_first_change_record( dir ))) free( record );
@@ -364,6 +352,12 @@
         release_object( dir->event );
     }
     release_object( dir->fd );
+
+    if (inotify_fd && list_empty( &change_list ))
+    {
+        release_object( inotify_fd );
+        inotify_fd = NULL;
+    }
 }
 
 static struct dir *
@@ -391,6 +385,87 @@
 
 #ifdef USE_INOTIFY
 
+#define HASH_SIZE 31
+
+struct inode {
+    struct list dirs;        /* directory handles watching this inode */
+    struct list ino_entry;   /* entry in the inode hash */
+    struct list wd_entry;    /* entry in the watch descriptor hash */
+    dev_t dev;               /* device number */
+    ino_t ino;               /* device's inode number */
+    int wd;                  /* inotify's watch descriptor */
+};
+
+struct list inode_hash[ HASH_SIZE ];
+struct list wd_hash[ HASH_SIZE ];
+
+static struct inode *inode_from_wd( int wd )
+{
+    struct list *bucket = &wd_hash[ wd % HASH_SIZE ];
+    struct inode *inode;
+
+    LIST_FOR_EACH_ENTRY( inode, bucket, struct inode, wd_entry )
+        if (inode->wd == wd)
+            return inode;
+
+    return NULL;
+}
+
+static inline struct list *get_hash_list( dev_t dev, ino_t ino )
+{
+    return &inode_hash[ (ino ^ dev) % HASH_SIZE ];
+}
+
+static struct inode *get_inode( dev_t dev, ino_t ino )
+{
+    struct list *bucket = get_hash_list( dev, ino );
+    struct inode *inode;
+
+    LIST_FOR_EACH_ENTRY( inode, bucket, struct inode, ino_entry )
+        if (inode->ino == ino && inode->dev == dev)
+             return inode;
+
+    return NULL;
+}
+
+static struct inode *create_inode( dev_t dev, ino_t ino )
+{
+    struct inode *inode;
+
+    inode = malloc( sizeof *inode );
+    if (inode)
+    {
+        list_init( &inode->dirs );
+        inode->ino = ino;
+        inode->dev = dev;
+        inode->wd = -1;
+        list_add_tail( get_hash_list( dev, ino ), &inode->ino_entry );
+    }
+    return inode;
+}
+
+static void inode_set_wd( struct inode *inode, int wd )
+{
+    if (inode->wd != -1)
+        list_remove( &inode->wd_entry );
+    inode->wd = wd;
+    list_add_tail( &wd_hash[ wd % HASH_SIZE ], &inode->wd_entry );
+}
+
+static void free_inode( struct inode *inode )
+{
+    if (!list_empty( &inode->dirs ))
+        return;
+
+    if (inode->wd != -1)
+    {
+        inotify_remove_watch( get_unix_fd( inotify_fd ), inode->wd );
+        list_remove( &inode->wd_entry );
+    }
+    list_remove( &inode->ino_entry );
+    free( inode );
+}
+
 static int inotify_get_poll_events( struct fd *fd );
 static void inotify_poll_event( struct fd *fd, int event );
 static int inotify_get_info( struct fd *fd );
@@ -442,12 +517,49 @@
     }
 }
 
+static unsigned int filter_from_event( struct inotify_event *ie )
+{
+    unsigned int filter = 0;
+
+    if (ie->mask & (IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE | IN_CREATE))
+        filter |= FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME;
+    if (ie->mask & IN_MODIFY)
+        filter |= FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE;
+    if (ie->mask & IN_ATTRIB)
+        filter |= FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY;
+    if (ie->mask & IN_ACCESS)
+        filter |= FILE_NOTIFY_CHANGE_LAST_ACCESS;
+    if (ie->mask & IN_CREATE)
+        filter |= FILE_NOTIFY_CHANGE_CREATION;
+
+    return filter;
+}
+
+static void inotify_notify_all( struct inotify_event *ie )
+{
+    struct inode *inode;
+    struct dir *dir;
+    unsigned int filter;
+
+    inode = inode_from_wd( ie->wd );
+    if (!inode)
+    {
+        fprintf( stderr, "no inode matches %d\n", ie->wd);
+        return;
+    }
+
+    filter = filter_from_event( ie );
+
+    LIST_FOR_EACH_ENTRY( dir, &inode->dirs, struct dir, in_entry )
+        if (filter & dir->filter)
+            inotify_do_change_notify( dir, ie );
+}
+
 static void inotify_poll_event( struct fd *fd, int event )
 {
     int r, ofs, unix_fd;
     char buffer[0x1000];
     struct inotify_event *ie;
-    struct dir *dir = get_fd_user( fd );
 
     unix_fd = get_unix_fd( fd );
     r = read( unix_fd, buffer, sizeof buffer );
@@ -464,7 +576,7 @@
             break;
         ofs += offsetof( struct inotify_event, name[ie->len] );
         if (ofs > r) break;
-        inotify_do_change_notify( dir, ie );
+        inotify_notify_all( ie );
     }
 }
 
@@ -473,26 +585,19 @@
     return 0;
 }
 
-static inline struct fd *create_inotify_fd( struct dir *dir )
+static inline struct fd *create_inotify_fd( void )
 {
     int unix_fd;
 
     unix_fd = inotify_init();
     if (unix_fd<0)
         return NULL;
-    return create_anonymous_fd( &inotify_fd_ops, unix_fd, &dir->obj );
+    return create_anonymous_fd( &inotify_fd_ops, unix_fd, NULL );
 }
 
-static int inotify_adjust_changes( struct dir *dir )
+static int map_flags( unsigned int filter )
 {
-    unsigned int filter = dir->filter;
     unsigned int mask = 0;
-    char link[32];
-
-    if (!dir->inotify_fd)
-    {
-        if (!(dir->inotify_fd = create_inotify_fd( dir ))) return 0;
-    }
 
     if (filter & FILE_NOTIFY_CHANGE_FILE_NAME)
         mask |= (IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE | IN_CREATE);
@@ -511,15 +616,131 @@
     if (filter & FILE_NOTIFY_CHANGE_SECURITY)
         mask |= IN_ATTRIB;
 
-    sprintf( link, "/proc/self/fd/%u", get_unix_fd( dir->fd ) );
-    dir->wd = inotify_add_watch( get_unix_fd( dir->inotify_fd ), link, mask );
-    if (dir->wd != -1)
-        set_fd_events( dir->inotify_fd, POLLIN );
+    return mask;
+}
+
+static int inotify_add_dir( char *path, unsigned int filter )
+{
+    int wd = inotify_add_watch( get_unix_fd( inotify_fd ),
+                                path, map_flags( filter ) );
+    if (wd != -1)
+        set_fd_events( inotify_fd, POLLIN );
+    return wd;
+}
+
+static unsigned int filter_from_inode( struct inode *inode )
+{
+    unsigned int filter = 0;
+    struct dir *dir;
+
+    LIST_FOR_EACH_ENTRY( dir, &inode->dirs, struct dir, in_entry )
+        filter |= dir->filter;
+
+    return filter;
+}
+
+static int init_inotify( void )
+{
+    int i;
+
+    if (inotify_fd)
+        return 1;
+
+    inotify_fd = create_inotify_fd();
+    if (!inotify_fd)
+        return 0;
+
+    for (i=0; i<HASH_SIZE; i++)
+    {
+        list_init( &inode_hash[i] );
+        list_init( &wd_hash[i] );
+    }
+
     return 1;
 }
 
+static int inotify_adjust_changes( struct dir *dir )
+{
+    unsigned int filter;
+    struct inode *inode;
+    struct stat st;
+    char path[32];
+    int wd, unix_fd;
+
+    if (!inotify_fd)
+        return 0;
+
+    unix_fd = get_unix_fd( dir->fd );
+
+    inode = dir->inode;
+    if (!inode)
+    {
+        /* check if this fd is already being watched */
+        if (-1 == fstat( unix_fd, &st ))
+            return 0;
+
+        inode = get_inode( st.st_dev, st.st_ino );
+        if (!inode)
+            inode = create_inode( st.st_dev, st.st_ino );
+        if (!inode)
+            return 0;
+        list_add_tail( &inode->dirs, &dir->in_entry );
+        dir->inode = inode;
+    }
+
+    filter = filter_from_inode( inode );
+
+    sprintf( path, "/proc/self/fd/%u", unix_fd );
+    wd = inotify_add_dir( path, filter );
+    if (wd == -1) return 0;
+
+    inode_set_wd( inode, wd );
+
+    return 1;
+}
+
+#else
+
+static int init_inotify( void )
+{
+    return 0;
+}
+
+static int inotify_adjust_changes( struct dir *dir )
+{
+    return 0;
+}
+
+static void free_inode( struct inode *inode )
+{
+    assert( 0 );
+}
+
 #endif  /* USE_INOTIFY */
 
+struct object *create_dir_obj( struct fd *fd )
+{
+    struct dir *dir;
+
+    dir = alloc_object( &dir_ops );
+    if (!dir)
+        return NULL;
+
+    list_init( &dir->change_q );
+    list_init( &dir->change_records );
+    dir->event = NULL;
+    dir->filter = 0;
+    dir->notified = 0;
+    dir->signaled = 0;
+    dir->want_data = 0;
+    dir->inode = NULL;
+    grab_object( fd );
+    dir->fd = fd;
+    set_fd_user( fd, &dir_fd_ops, &dir->obj );
+
+    return &dir->obj;
+}
+
 /* enable change notifications for a directory */
 DECL_HANDLER(read_directory_changes)
 {
@@ -556,6 +777,7 @@
     /* assign it once */
     if (!dir->filter)
     {
+        init_inotify();
         insert_change( dir );
         dir->filter = req->filter;
         dir->want_data = req->want_data;
@@ -570,9 +792,7 @@
         reset_event( event );
 
     /* setup the real notification */
-#ifdef USE_INOTIFY
     if (!inotify_adjust_changes( dir ))
-#endif
         dnotify_adjust_changes( dir );
 
     set_error(STATUS_PENDING);