Snapshot of commit d5ec1d5018ed24f1b4f32b1d09df6dbd7e2fc425

from branch master of git://git.jetbrains.org/idea/community.git
diff --git a/native/fsNotifier/linux/fsnotifier.h b/native/fsNotifier/linux/fsnotifier.h
new file mode 100644
index 0000000..0b3e527
--- /dev/null
+++ b/native/fsNotifier/linux/fsnotifier.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2000-2012 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __FSNOTIFIER_H
+#define __FSNOTIFIER_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+
+// logging
+void userlog(int priority, const char* format, ...);
+
+#define CHECK_NULL(p, r) if (p == NULL)  { userlog(LOG_ERR, "out of memory"); return r; }
+
+
+// variable-length array
+typedef struct __array array;
+
+array* array_create(int initial_capacity);
+int array_size(array* a);
+void* array_push(array* a, void* element);
+void* array_pop(array* a);
+void array_put(array* a, int index, void* element);
+void* array_get(array* a, int index);
+void array_delete(array* a);
+void array_delete_vs_data(array* a);
+
+
+// poor man's hash table
+typedef struct __table table;
+
+table* table_create(int capacity);
+void* table_put(table* t, int key, void* value);
+void* table_get(table* t, int key);
+void table_delete(table* t);
+
+
+// inotify subsystem
+enum {
+  ERR_IGNORE = -1,
+  ERR_CONTINUE = -2,
+  ERR_ABORT = -3
+};
+
+bool init_inotify();
+void set_inotify_callback(void (* callback)(char*, int));
+int get_inotify_fd();
+int get_watch_count();
+bool watch_limit_reached();
+int watch(const char* root, array* mounts);
+void unwatch(int id);
+bool process_inotify_input();
+void close_inotify();
+
+
+// reads one line from stream, trims trailing carriage return if any
+// returns pointer to the internal buffer (will be overwritten on next call)
+char* read_line(FILE* stream);
+
+
+// path comparison
+bool is_parent_path(const char* parent_path, const char* child_path);
+
+
+#endif
diff --git a/native/fsNotifier/linux/inotify.c b/native/fsNotifier/linux/inotify.c
new file mode 100644
index 0000000..75a97ab
--- /dev/null
+++ b/native/fsNotifier/linux/inotify.c
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2000-2012 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "fsnotifier.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <linux/limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <syslog.h>
+#include <unistd.h>
+
+
+#define WATCH_COUNT_NAME "/proc/sys/fs/inotify/max_user_watches"
+
+#define DEFAULT_SUBDIR_COUNT 5
+
+typedef struct __watch_node {
+  char* name;
+  int wd;
+  struct __watch_node* parent;
+  array* kids;
+} watch_node;
+
+static int inotify_fd = -1;
+static int watch_count = 0;
+static table* watches;
+static bool limit_reached = false;
+static void (* callback)(char*, int) = NULL;
+
+#define EVENT_SIZE (sizeof(struct inotify_event))
+#define EVENT_BUF_LEN (2048 * (EVENT_SIZE + 16))
+static char event_buf[EVENT_BUF_LEN];
+
+
+static void read_watch_descriptors_count() {
+  FILE* f = fopen(WATCH_COUNT_NAME, "r");
+  if (f == NULL) {
+    userlog(LOG_ERR, "can't open %s: %s", WATCH_COUNT_NAME, strerror(errno));
+    return;
+  }
+
+  char* str = read_line(f);
+  if (str == NULL) {
+    userlog(LOG_ERR, "can't read from %s", WATCH_COUNT_NAME);
+  }
+  else {
+    watch_count = atoi(str);
+  }
+
+  fclose(f);
+}
+
+
+bool init_inotify() {
+  inotify_fd = inotify_init();
+  if (inotify_fd < 0) {
+    userlog(LOG_ERR, "inotify_init: %s", strerror(errno));
+    return false;
+  }
+  userlog(LOG_DEBUG, "inotify fd: %d", get_inotify_fd());
+
+  read_watch_descriptors_count();
+  if (watch_count <= 0) {
+    close(inotify_fd);
+    inotify_fd = -1;
+    return false;
+  }
+  userlog(LOG_INFO, "inotify watch descriptors: %d", watch_count);
+
+  watches = table_create(watch_count);
+  if (watches == NULL) {
+    userlog(LOG_ERR, "out of memory");
+    close(inotify_fd);
+    inotify_fd = -1;
+    return false;
+  }
+
+  return true;
+}
+
+
+inline void set_inotify_callback(void (* _callback)(char*, int)) {
+  callback = _callback;
+}
+
+
+inline int get_inotify_fd() {
+  return inotify_fd;
+}
+
+
+inline int get_watch_count() {
+  return watch_count;
+}
+
+
+inline bool watch_limit_reached() {
+  return limit_reached;
+}
+
+
+#define EVENT_MASK IN_MODIFY | IN_ATTRIB | IN_CREATE | IN_DELETE | IN_MOVE | IN_DELETE_SELF
+
+static int add_watch(const char* path, watch_node* parent) {
+  int wd = inotify_add_watch(inotify_fd, path, EVENT_MASK);
+  if (wd < 0) {
+    if (errno == EACCES || errno == ENOENT) {
+      userlog(LOG_DEBUG, "inotify_add_watch(%s): %s", path, strerror(errno));
+      return ERR_IGNORE;
+    }
+    else if (errno == ENOSPC) {
+      userlog(LOG_WARNING, "inotify_add_watch(%s): %s", path, strerror(errno));
+      limit_reached = true;
+      return ERR_CONTINUE;
+    }
+    else {
+      userlog(LOG_ERR, "inotify_add_watch(%s): %s", path, strerror(errno));
+      return ERR_ABORT;
+    }
+  }
+  else {
+    userlog(LOG_DEBUG, "watching %s: %d", path, wd);
+  }
+
+  watch_node* node = table_get(watches, wd);
+  if (node != NULL) {
+    if (node->wd != wd) {
+      userlog(LOG_ERR, "table error: corruption at %d:%s / %d:%s)", wd, path, node->wd, node->name);
+      return ERR_ABORT;
+    }
+    else if (strcmp(node->name, path) != 0) {
+      char buf1[PATH_MAX], buf2[PATH_MAX];
+      const char* normalized1 = realpath(node->name, buf1);
+      const char* normalized2 = realpath(path, buf2);
+      if (normalized1 == NULL || normalized2 == NULL || strcmp(normalized1, normalized2) != 0) {
+        userlog(LOG_ERR, "table error: collision at %d (new %s, existing %s)", wd, path, node->name);
+        return ERR_ABORT;
+      }
+      else {
+        userlog(LOG_INFO, "intersection at %d: (new %s, existing %s, real %s)", wd, path, node->name, normalized1);
+        return ERR_IGNORE;
+      }
+    }
+
+    return wd;
+  }
+
+  node = malloc(sizeof(watch_node));
+
+  CHECK_NULL(node, ERR_ABORT);
+  node->name = strdup(path);
+  CHECK_NULL(node->name, ERR_ABORT);
+  node->wd = wd;
+  node->parent = parent;
+  node->kids = NULL;
+
+  if (parent != NULL) {
+    if (parent->kids == NULL) {
+      parent->kids = array_create(DEFAULT_SUBDIR_COUNT);
+      CHECK_NULL(parent->kids, ERR_ABORT);
+    }
+    CHECK_NULL(array_push(parent->kids, node), ERR_ABORT);
+  }
+
+  if (table_put(watches, wd, node) == NULL) {
+    userlog(LOG_ERR, "table error: unable to put (%d:%s)", wd, path);
+    return ERR_ABORT;
+  }
+
+  return wd;
+}
+
+
+static void rm_watch(int wd, bool update_parent) {
+  watch_node* node = table_get(watches, wd);
+  if (node == NULL) {
+    return;
+  }
+
+  userlog(LOG_DEBUG, "unwatching %s: %d (%p)", node->name, node->wd, node);
+
+  if (inotify_rm_watch(inotify_fd, node->wd) < 0) {
+    userlog(LOG_DEBUG, "inotify_rm_watch(%d:%s): %s", node->wd, node->name, strerror(errno));
+  }
+
+  for (int i=0; i<array_size(node->kids); i++) {
+    watch_node* kid = array_get(node->kids, i);
+    if (kid != NULL) {
+      rm_watch(kid->wd, false);
+    }
+  }
+
+  if (update_parent && node->parent != NULL) {
+    for (int i=0; i<array_size(node->parent->kids); i++) {
+      if (array_get(node->parent->kids, i) == node) {
+        array_put(node->parent->kids, i, NULL);
+        break;
+      }
+    }
+  }
+
+  free(node->name);
+  array_delete(node->kids);
+  free(node);
+  table_put(watches, wd, NULL);
+}
+
+
+static int walk_tree(const char* path, watch_node* parent, bool recursive, array* mounts) {
+  for (int j=0; j<array_size(mounts); j++) {
+    char* mount = array_get(mounts, j);
+    if (strncmp(path, mount, strlen(mount)) == 0) {
+      userlog(LOG_DEBUG, "watch path '%s' crossed mount point '%s' - skipping", path, mount);
+      return ERR_IGNORE;
+    }
+  }
+
+  DIR* dir = NULL;
+  if (recursive) {
+    if ((dir = opendir(path)) == NULL) {
+      if (errno == EACCES || errno == ENOENT || errno == ENOTDIR) {
+        userlog(LOG_DEBUG, "opendir(%s): %d", path, errno);
+        return ERR_IGNORE;
+      }
+      else {
+        userlog(LOG_ERR, "opendir(%s): %s", path, strerror(errno));
+        return ERR_CONTINUE;
+      }
+    }
+  }
+
+  int id = add_watch(path, parent);
+
+  if (dir == NULL) {
+    return id;
+  }
+  else if (id < 0) {
+    closedir(dir);
+    return id;
+  }
+
+  char subdir[PATH_MAX];
+  strcpy(subdir, path);
+  if (subdir[strlen(subdir) - 1] != '/') {
+    strcat(subdir, "/");
+  }
+  char* p = subdir + strlen(subdir);
+
+  struct dirent* entry;
+  while ((entry = readdir(dir)) != NULL) {
+    if (entry->d_type != DT_DIR ||
+        strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
+      continue;
+    }
+
+    strcpy(p, entry->d_name);
+
+    int subdir_id = walk_tree(subdir, table_get(watches, id), recursive, mounts);
+    if (subdir_id < 0 && subdir_id != ERR_IGNORE) {
+      rm_watch(id, true);
+      id = subdir_id;
+      break;
+    }
+  }
+
+  closedir(dir);
+  return id;
+}
+
+
+int watch(const char* root, array* mounts) {
+  bool recursive = true;
+  if (root[0] == '|') {
+    root++;
+    recursive = false;
+  }
+
+  struct stat st;
+  if (stat(root, &st) != 0) {
+    if (errno == EACCES) {
+      return ERR_IGNORE;
+    }
+    else if (errno == ENOENT) {
+      return ERR_CONTINUE;
+    }
+    userlog(LOG_ERR, "stat(%s): %s", root, strerror(errno));
+    return ERR_ABORT;
+  }
+
+  if (S_ISREG(st.st_mode)) {
+    recursive = false;
+  }
+  else if (!S_ISDIR(st.st_mode)) {
+    userlog(LOG_WARNING, "unexpected node type: %s, %d", root, st.st_mode);
+    return ERR_IGNORE;
+  }
+
+  return walk_tree(root, NULL, recursive, mounts);
+}
+
+
+void unwatch(int id) {
+  rm_watch(id, true);
+}
+
+
+static bool process_inotify_event(struct inotify_event* event) {
+  watch_node* node = table_get(watches, event->wd);
+  if (node == NULL) {
+    return true;
+  }
+
+  bool is_dir = (event->mask & IN_ISDIR) == IN_ISDIR;
+  userlog(LOG_DEBUG, "inotify: wd=%d mask=%d dir=%d name=%s", event->wd, event->mask & (~IN_ISDIR), is_dir, node->name);
+
+  char path[PATH_MAX];
+  strcpy(path, node->name);
+  if (event->len > 0) {
+    if (path[strlen(path) - 1] != '/') {
+      strcat(path, "/");
+    }
+    strcat(path, event->name);
+  }
+
+  if (is_dir && ((event->mask & IN_CREATE) == IN_CREATE || (event->mask & IN_MOVED_TO) == IN_MOVED_TO)) {
+    int result = walk_tree(path, node, true, NULL);
+    if (result < 0 && result != ERR_IGNORE && result != ERR_CONTINUE) {
+      return false;
+    }
+  }
+
+  if (is_dir && ((event->mask & IN_DELETE) == IN_DELETE || (event->mask & IN_MOVED_FROM) == IN_MOVED_FROM)) {
+    for (int i=0; i<array_size(node->kids); i++) {
+      watch_node* kid = array_get(node->kids, i);
+      if (kid != NULL && strcmp(kid->name, path) == 0) {
+        rm_watch(kid->wd, false);
+        array_put(node->kids, i, NULL);
+        break;
+      }
+    }
+  }
+
+  if (callback != NULL) {
+    (*callback)(path, event->mask);
+  }
+  return true;
+}
+
+
+bool process_inotify_input() {
+  ssize_t len = read(inotify_fd, event_buf, EVENT_BUF_LEN);
+  if (len < 0) {
+    userlog(LOG_ERR, "read: %s", strerror(errno));
+    return false;
+  }
+
+  int i = 0;
+  while (i < len) {
+    struct inotify_event* event = (struct inotify_event*) &event_buf[i];
+    i += EVENT_SIZE + event->len;
+
+    if (event->mask & IN_IGNORED) {
+      continue;
+    }
+    if (event->mask & IN_Q_OVERFLOW) {
+      userlog(LOG_ERR, "event queue overflow");
+      continue;
+    }
+
+    if (!process_inotify_event(event)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+
+void close_inotify() {
+  if (watches != NULL) {
+    table_delete(watches);
+  }
+
+  if (inotify_fd >= 0) {
+    close(inotify_fd);
+  }
+}
diff --git a/native/fsNotifier/linux/main.c b/native/fsNotifier/linux/main.c
new file mode 100644
index 0000000..606e914
--- /dev/null
+++ b/native/fsNotifier/linux/main.c
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2000-2012 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "fsnotifier.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <mntent.h>
+#include <paths.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/select.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#define LOG_ENV "FSNOTIFIER_LOG_LEVEL"
+#define LOG_ENV_DEBUG "debug"
+#define LOG_ENV_INFO "info"
+#define LOG_ENV_WARNING "warning"
+#define LOG_ENV_ERROR "error"
+#define LOG_ENV_OFF "off"
+
+#define VERSION "1.2"
+#define VERSION_MSG "fsnotifier " VERSION "\n"
+
+#define USAGE_MSG \
+    "fsnotifier - IntelliJ IDEA companion program for watching and reporting file and directory structure modifications.\n\n" \
+    "fsnotifier utilizes \"user\" facility of syslog(3) - messages usually can be found in /var/log/user.log.\n" \
+    "Verbosity is regulated via " LOG_ENV " environment variable, possible values are: " \
+    LOG_ENV_DEBUG ", " LOG_ENV_INFO ", " LOG_ENV_WARNING ", " LOG_ENV_ERROR ", " LOG_ENV_OFF "; latter is the default.\n\n" \
+    "Use 'fsnotifier --selftest' to perform some self-diagnostics (output will be logged and printed to console).\n"
+
+#define HELP_MSG \
+    "Try 'fsnotifier --help' for more information.\n"
+
+#define INOTIFY_LIMIT_MSG \
+    "The current <b>inotify</b>(7) watch limit of %d is too low. " \
+    "<a href=\"http://confluence.jetbrains.net/display/IDEADEV/Inotify+Watches+Limit\">More details.</a>\n"
+
+typedef struct {
+  char* name;
+  int id;
+} watch_root;
+
+static array* roots = NULL;
+
+static bool show_warning = true;
+static bool self_test = false;
+
+static void init_log();
+static void run_self_test();
+static void main_loop();
+static bool read_input();
+static bool update_roots(array* new_roots);
+static void unregister_roots();
+static bool register_roots(array* new_roots, array* unwatchable, array* mounts);
+static array* unwatchable_mounts();
+static void inotify_callback(char* path, int event);
+static void output(const char* format, ...);
+
+
+int main(int argc, char** argv) {
+  if (argc > 1) {
+    if (strcmp(argv[1], "--help") == 0) {
+      printf(USAGE_MSG);
+      return 0;
+    }
+    else if (strcmp(argv[1], "--version") == 0) {
+      printf(VERSION_MSG);
+      return 0;
+    }
+    else if (strcmp(argv[1], "--selftest") == 0) {
+      self_test = true;
+    }
+    else {
+      printf("unrecognized option: %s\n", argv[1]);
+      printf(HELP_MSG);
+      return 1;
+    }
+  }
+
+  init_log();
+  if (!self_test) {
+    userlog(LOG_INFO, "started (v." VERSION ")");
+  }
+  else {
+    userlog(LOG_INFO, "started (self-test mode) (v." VERSION ")");
+  }
+
+  setvbuf(stdin, NULL, _IONBF, 0);
+  setvbuf(stdout, NULL, _IONBF, 0);
+
+  roots = array_create(20);
+  if (init_inotify() && roots != NULL) {
+    set_inotify_callback(&inotify_callback);
+
+    if (!self_test) {
+      main_loop();
+    }
+    else {
+      run_self_test();
+    }
+
+    unregister_roots();
+  }
+  else {
+    printf("GIVEUP\n");
+  }
+  close_inotify();
+  array_delete(roots);
+
+  userlog(LOG_INFO, "finished");
+  closelog();
+
+  return 0;
+}
+
+
+static void init_log() {
+  char* env_level = getenv(LOG_ENV);
+  int level = LOG_EMERG;
+  if (env_level != NULL) {
+    if (strcmp(env_level, LOG_ENV_DEBUG) == 0)  level = LOG_DEBUG;
+    else if (strcmp(env_level, LOG_ENV_INFO) == 0)  level = LOG_INFO;
+    else if (strcmp(env_level, LOG_ENV_WARNING) == 0)  level = LOG_WARNING;
+    else if (strcmp(env_level, LOG_ENV_ERROR) == 0)  level = LOG_ERR;
+  }
+
+  if (self_test) {
+    level = LOG_DEBUG;
+  }
+
+  char ident[32];
+  snprintf(ident, sizeof(ident), "fsnotifier[%d]", getpid());
+  openlog(ident, 0, LOG_USER);
+  setlogmask(LOG_UPTO(level));
+}
+
+
+void userlog(int priority, const char* format, ...) {
+  va_list ap;
+
+  va_start(ap, format);
+  vsyslog(priority, format, ap);
+  va_end(ap);
+
+  if (self_test) {
+    const char* level = "debug";
+    switch (priority) {
+      case LOG_ERR:  level = "error"; break;
+      case LOG_WARNING:  level = " warn"; break;
+      case LOG_INFO:  level = " info"; break;
+    }
+    printf("fsnotifier[%d] %s: ", getpid(), level);
+
+    va_start(ap, format);
+    vprintf(format, ap);
+    va_end(ap);
+
+    printf("\n");
+  }
+}
+
+
+static void run_self_test() {
+  array* test_roots = array_create(1);
+  char* cwd = malloc(PATH_MAX);
+  if (getcwd(cwd, PATH_MAX) == NULL) {
+    strncpy(cwd, ".", PATH_MAX);
+  }
+  array_push(test_roots, cwd);
+  update_roots(test_roots);
+}
+
+
+static void main_loop() {
+  int input_fd = fileno(stdin), inotify_fd = get_inotify_fd();
+  int nfds = (inotify_fd > input_fd ? inotify_fd : input_fd) + 1;
+  fd_set rfds;
+  bool go_on = true;
+
+  while (go_on) {
+    FD_ZERO(&rfds);
+    FD_SET(input_fd, &rfds);
+    FD_SET(inotify_fd, &rfds);
+    if (select(nfds, &rfds, NULL, NULL, NULL) < 0) {
+      userlog(LOG_ERR, "select: %s", strerror(errno));
+      go_on = false;
+    }
+    else if (FD_ISSET(input_fd, &rfds)) {
+      go_on = read_input();
+    }
+    else if (FD_ISSET(inotify_fd, &rfds)) {
+      go_on = process_inotify_input();
+    }
+  }
+}
+
+
+static bool read_input() {
+  char* line = read_line(stdin);
+  userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
+
+  if (line == NULL || strcmp(line, "EXIT") == 0) {
+    userlog(LOG_INFO, "exiting: %s", line);
+    return false;
+  }
+
+  if (strcmp(line, "ROOTS") == 0) {
+    array* new_roots = array_create(20);
+    CHECK_NULL(new_roots, false);
+
+    while (1) {
+      line = read_line(stdin);
+      userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
+      if (line == NULL || strlen(line) == 0) {
+        return false;
+      }
+      else if (strcmp(line, "#") == 0) {
+        break;
+      }
+      else {
+        int l = strlen(line);
+        if (l > 1 && line[l-1] == '/')  line[l-1] = '\0';
+        CHECK_NULL(array_push(new_roots, strdup(line)), false);
+      }
+    }
+
+    return update_roots(new_roots);
+  }
+
+  userlog(LOG_INFO, "unrecognised command: %s", line);
+  return true;
+}
+
+
+static bool update_roots(array* new_roots) {
+  userlog(LOG_INFO, "updating roots (curr:%d, new:%d)", array_size(roots), array_size(new_roots));
+
+  unregister_roots();
+
+  if (array_size(new_roots) == 0) {
+    output("UNWATCHEABLE\n#\n");
+    array_delete(new_roots);
+    return true;
+  }
+  else if (array_size(new_roots) == 1 && strcmp(array_get(new_roots, 0), "/") == 0) {  // refuse to watch entire tree
+    output("UNWATCHEABLE\n/\n#\n");
+    userlog(LOG_INFO, "unwatchable: /");
+    array_delete_vs_data(new_roots);
+    return true;
+  }
+
+  array* mounts = unwatchable_mounts();
+  if (mounts == NULL) {
+    return false;
+  }
+
+  array* unwatchable = array_create(20);
+  if (!register_roots(new_roots, unwatchable, mounts)) {
+    return false;
+  }
+
+  output("UNWATCHEABLE\n");
+  for (int i=0; i<array_size(unwatchable); i++) {
+    char* s = array_get(unwatchable, i);
+    output("%s\n", s);
+    userlog(LOG_INFO, "unwatchable: %s", s);
+  }
+  output("#\n");
+
+  array_delete_vs_data(unwatchable);
+  array_delete_vs_data(mounts);
+  array_delete_vs_data(new_roots);
+
+  return true;
+}
+
+
+static void unregister_roots() {
+  watch_root* root;
+  while ((root = array_pop(roots)) != NULL) {
+    userlog(LOG_INFO, "unregistering root: %s", root->name);
+    unwatch(root->id);
+    free(root->name);
+    free(root);
+  };
+}
+
+
+static bool register_roots(array* new_roots, array* unwatchable, array* mounts) {
+  for (int i=0; i<array_size(new_roots); i++) {
+    char* new_root = array_get(new_roots, i);
+    char* unflattened = new_root;
+    if (unflattened[0] == '|') ++unflattened;
+    userlog(LOG_INFO, "registering root: %s", new_root);
+
+    if (unflattened[0] != '/') {
+      userlog(LOG_WARNING, "  ... not valid, skipped");
+      continue;
+    }
+
+    array* inner_mounts = array_create(5);
+    CHECK_NULL(inner_mounts, false);
+
+    bool skip = false;
+    for (int j=0; j<array_size(mounts); j++) {
+      char* mount = array_get(mounts, j);
+      if (is_parent_path(mount, unflattened)) {
+        userlog(LOG_DEBUG, "watch root '%s' is under mount point '%s' - skipping", unflattened, mount);
+        CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
+        skip = true;
+        break;
+      }
+      else if (is_parent_path(unflattened, mount)) {
+        userlog(LOG_DEBUG, "watch root '%s' contains mount point '%s' - partial watch", unflattened, mount);
+        char* copy = strdup(mount);
+        CHECK_NULL(array_push(unwatchable, copy), false);
+        CHECK_NULL(array_push(inner_mounts, copy), false);
+      }
+    }
+    if (skip) {
+      continue;
+    }
+
+    int id = watch(new_root, inner_mounts);
+    array_delete(inner_mounts);
+
+    if (id >= 0) {
+      watch_root* root = malloc(sizeof(watch_root));
+      CHECK_NULL(root, false);
+      root->id = id;
+      root->name = strdup(new_root);
+      CHECK_NULL(root->name, false);
+      CHECK_NULL(array_push(roots, root), false);
+    }
+    else if (id == ERR_ABORT) {
+      return false;
+    }
+    else if (id != ERR_IGNORE) {
+      if (show_warning && watch_limit_reached()) {
+        int limit = get_watch_count();
+        userlog(LOG_WARNING, "watch limit (%d) reached", limit);
+        output("MESSAGE\n" INOTIFY_LIMIT_MSG, limit);
+        show_warning = false;  // warn only once
+      }
+      CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
+    }
+  }
+
+  return true;
+}
+
+
+static bool is_watchable(const char* fs) {
+  // don't watch special and network filesystems
+  return !(strncmp(fs, "dev", 3) == 0 || strcmp(fs, "proc") == 0 || strcmp(fs, "sysfs") == 0 || strcmp(fs, MNTTYPE_SWAP) == 0 ||
+           strncmp(fs, "fuse", 4) == 0 || strcmp(fs, "cifs") == 0 || strcmp(fs, MNTTYPE_NFS) == 0);
+}
+
+static array* unwatchable_mounts() {
+  FILE* mtab = setmntent(_PATH_MOUNTED, "r");
+  if (mtab == NULL) {
+    userlog(LOG_ERR, "cannot open " _PATH_MOUNTED);
+    return NULL;
+  }
+
+  array* mounts = array_create(20);
+  CHECK_NULL(mounts, NULL);
+
+  struct mntent* ent;
+  while ((ent = getmntent(mtab)) != NULL) {
+    userlog(LOG_DEBUG, "mtab: %s : %s", ent->mnt_dir, ent->mnt_type);
+    if (strcmp(ent->mnt_type, MNTTYPE_IGNORE) != 0 && !is_watchable(ent->mnt_type)) {
+      CHECK_NULL(array_push(mounts, strdup(ent->mnt_dir)), NULL);
+    }
+  }
+
+  endmntent(mtab);
+  return mounts;
+}
+
+
+static void inotify_callback(char* path, int event) {
+  if (event & IN_CREATE || event & IN_MOVED_TO) {
+    output("CREATE\n%s\nCHANGE\n%s\n", path, path);
+    userlog(LOG_DEBUG, "CREATE: %s", path);
+    return;
+  }
+
+  if (event & IN_MODIFY) {
+    output("CHANGE\n%s\n", path);
+    userlog(LOG_DEBUG, "CHANGE: %s", path);
+    return;
+  }
+
+  if (event & IN_ATTRIB) {
+    output("STATS\n%s\n", path);
+    userlog(LOG_DEBUG, "STATS: %s", path);
+    return;
+  }
+
+  if (event & IN_DELETE || event & IN_MOVED_FROM) {
+    output("DELETE\n%s\n", path);
+    userlog(LOG_DEBUG, "DELETE: %s", path);
+    return;
+  }
+
+  if (event & IN_UNMOUNT) {
+    output("RESET\n");
+    userlog(LOG_DEBUG, "RESET");
+    return;
+  }
+}
+
+
+static void output(const char* format, ...) {
+  if (self_test) {
+    return;
+  }
+
+  va_list ap;
+  va_start(ap, format);
+  vprintf(format, ap);
+  va_end(ap);
+}
diff --git a/native/fsNotifier/linux/make.sh b/native/fsNotifier/linux/make.sh
new file mode 100755
index 0000000..4dc3657
--- /dev/null
+++ b/native/fsNotifier/linux/make.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+echo "compiling 32-bit version"
+clang -m32 -O2 -Wall -std=c99 -D_BSD_SOURCE -D_XOPEN_SOURCE=500 -o fsnotifier main.c inotify.c util.c
+if [ $? -eq 0 ] ; then
+  echo "compiling 64-bit version"
+  clang -m64 -O2 -Wall -std=c99 -D_BSD_SOURCE -D_XOPEN_SOURCE=500 -o fsnotifier64 main.c inotify.c util.c
+  if [ $? -eq 0 ] ; then
+    chmod 755 fsnotifier fsnotifier64
+  fi
+fi
diff --git a/native/fsNotifier/linux/util.c b/native/fsNotifier/linux/util.c
new file mode 100644
index 0000000..179b310
--- /dev/null
+++ b/native/fsNotifier/linux/util.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2000-2012 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "fsnotifier.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+#define REALLOC_FACTOR 2
+
+struct __array {
+  void** data;
+  int size;
+  int capacity;
+};
+
+static bool array_realloc(array* a) {
+  if (a->size == a->capacity) {
+    int new_cap = a->capacity * REALLOC_FACTOR;
+    void* new_ptr = realloc(a->data, sizeof(void*) * new_cap);
+    if (new_ptr == NULL) {
+      return false;
+    }
+    a->capacity = new_cap;
+    a->data = new_ptr;
+  }
+  return true;
+}
+
+array* array_create(int initial_capacity) {
+  array* a = (array*) malloc(sizeof(array));
+  if (a == NULL) {
+    return NULL;
+  }
+
+  a->data = calloc(sizeof(void*), initial_capacity);
+  if (a->data == NULL) {
+    free(a);
+    return NULL;
+  }
+
+  a->capacity = initial_capacity;
+  a->size = 0;
+
+  return a;
+}
+
+inline int array_size(array* a) {
+  return (a != NULL ? a->size : 0);
+}
+
+void* array_push(array* a, void* element) {
+  if (a == NULL || !array_realloc(a)) {
+    return NULL;
+  }
+  a->data[a->size++] = element;
+  return element;
+}
+
+void* array_pop(array* a) {
+  if (a != NULL && a->size > 0) {
+    return a->data[--a->size];
+  }
+  else {
+    return NULL;
+  }
+}
+
+void array_put(array* a, int index, void* element) {
+  if (a != NULL && index >=0 && index < a->capacity) {
+    a->data[index] = element;
+    if (a->size <= index) {
+      a->size = index + 1;
+    }
+  }
+}
+
+void* array_get(array* a, int index) {
+  if (a != NULL && index >= 0 && index < a->size) {
+    return a->data[index];
+  }
+  else {
+    return NULL;
+  }
+}
+
+void array_delete(array* a) {
+  if (a != NULL) {
+    free(a->data);
+    free(a);
+  }
+}
+
+void array_delete_vs_data(array* a) {
+  if (a != NULL) {
+    for (int i=0; i<a->size; i++) {
+      if (a->data[i] != NULL) {
+        free(a->data[i]);
+      }
+    }
+    array_delete(a);
+  }
+}
+
+
+struct __table {
+  void** data;
+  int capacity;
+};
+
+table* table_create(int capacity) {
+  table* t = malloc(sizeof(table));
+  if (t == NULL) {
+    return NULL;
+  }
+
+  t->data = calloc(sizeof(void*), capacity);
+  if (t->data == NULL) {
+    free(t);
+    return NULL;
+  }
+  memset(t->data, 0, sizeof(void*) * capacity);
+
+  t->capacity = capacity;
+
+  return t;
+}
+
+static inline int wrap(int key, table* t) {
+  return (t != NULL ? key % t->capacity : -1);
+}
+
+// todo: resolve collisions (?)
+void* table_put(table* t, int key, void* value) {
+  int k = wrap(key, t);
+  if (k < 0 || (value != NULL && t->data[k] != NULL)) {
+    return NULL;
+  }
+  else {
+    return t->data[k] = value;
+  }
+}
+
+void* table_get(table* t, int key) {
+  int k = wrap(key, t);
+  if (k < 0) {
+    return NULL;
+  }
+  else {
+    return t->data[k];
+  }
+}
+
+void table_delete(table* t) {
+  if (t != NULL) {
+    free(t->data);
+    free(t);
+  }
+}
+
+
+#define INPUT_BUF_LEN 2048
+static char input_buf[INPUT_BUF_LEN];
+
+char* read_line(FILE* stream) {
+  char* retval = fgets(input_buf, INPUT_BUF_LEN, stream);
+  if (retval == NULL || feof(stream)) {
+    return NULL;
+  }
+  int pos = strlen(input_buf) - 1;
+  if (input_buf[pos] == '\n') {
+    input_buf[pos] = '\0';
+  }
+  return input_buf;
+}
+
+
+bool is_parent_path(const char* parent_path, const char* child_path) {
+  size_t parent_len = strlen(parent_path);
+  return strncmp(parent_path, child_path, parent_len) == 0 &&
+         (parent_len == strlen(child_path) || child_path[parent_len] == '/');
+}