Browse Source

fs v 1.9 init

221V 3 years ago
parent
commit
5d8596e75d

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+deps/
+ebin/*.beam

+ 13 - 0
LICENSE

@@ -0,0 +1,13 @@
+Copyright (c) 2013 Vladimir Kirillov <proger@hackndev.com>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 46 - 1
README.md

@@ -1,2 +1,47 @@
-# fs
+FS Listener - fs v 1.9 fork
+===========
 
+Backends
+--------
+
+* Mac [fsevent](https://github.com/thibaudgg/rb-fsevent)
+* Linux [inotify](https://github.com/rvoicilas/inotify-tools/wiki)
+* Windows [inotify-win](https://github.com/thekid/inotify-win)
+
+NOTE: On Linux you need to install inotify-tools.
+
+### Subscribe to Notifications
+
+```erlang
+> fs:subscribe(). % the pid will receive events as messages
+> flush(). 
+Shell got {<0.47.0>,
+           {fs,file_event},
+           {"/Users/5HT/synrc/fs/src/README.md",[closed,modified]}}
+```
+
+### List Events from Backend
+
+```erlang
+> fs:known_events(). % returns events known by your current backend
+[mustscansubdirs,userdropped,kerneldropped,eventidswrapped,
+ historydone,rootchanged,mount,unmount,created,removed,
+ inodemetamod,renamed,modified,finderinfomod,changeowner,
+ xattrmod,isfile,isdir,issymlink,ownevent]
+```
+
+### Sample Subscriber
+
+```erlang
+> fs:start_logger(). % starts a sample process that logs events with error_logger
+=INFO REPORT==== 28-Aug-2013::19:36:26 ===
+file_event: "/tank/proger/erlfsmon/src/4913" [closed,modified]
+```
+
+Credits
+-------
+
+* Vladimir Kirillov
+* Maxim Sokhatsky
+
+OM A HUM

+ 34 - 0
c_src/bsd/main.c

@@ -0,0 +1,34 @@
+#include <sys/time.h>
+#include <sys/event.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(int argc, char *argv[]) {
+    struct kevent event;
+    struct kevent change;
+    int fd, kq, nev;
+    if ((fd = open(argv[1], O_RDONLY)) == -1) return 1;
+    EV_SET(&change, fd, EVFILT_VNODE , EV_ADD
+                                     | EV_ENABLE
+                                     | EV_DISABLE
+                                     | EV_CLEAR
+                                     | EV_DELETE
+                                     | EV_EOF
+                                     | EV_RECEIPT
+                                     | EV_DISPATCH
+                                     | EV_ONESHOT,
+                                       NOTE_DELETE
+                                     | NOTE_RENAME
+                                     | NOTE_EXTEND
+                                     | NOTE_ATTRIB
+                                     | NOTE_LINK
+                                     | NOTE_REVOKE
+                                     | NOTE_WRITE, 0, 0);
+    if ((kq = kqueue()) == -1) return 1;
+    nev = kevent(kq, &change, 1, &event, 1, NULL);
+    if (nev < 0) { return 1; } else if (nev > 0) { if (event.flags & EV_ERROR) { return 1; } }
+    close(kq);
+    return 0;
+}

+ 180 - 0
c_src/mac/cli.c

@@ -0,0 +1,180 @@
+#include <getopt.h>
+#include "cli.h"
+
+const char* cli_info_purpose = "A flexible command-line interface for the FSEvents API";
+const char* cli_info_usage = "Usage: fsevent_watch [OPTIONS]... [PATHS]...";
+const char* cli_info_help[] = {
+  "  -h, --help                you're looking at it",
+  "  -V, --version             print version number and exit",
+  "  -p, --show-plist          display the embedded Info.plist values",
+  "  -s, --since-when=EventID  fire historical events since ID",
+  "  -l, --latency=seconds     latency period (default='0.5')",
+  "  -n, --no-defer            enable no-defer latency modifier",
+  "  -r, --watch-root          watch for when the root path has changed",
+  // "  -i, --ignore-self         ignore current process",
+  "  -F, --file-events         provide file level event data",
+  "  -f, --format=name         output format (ignored)",
+  0
+};
+
+static void default_args (struct cli_info* args_info)
+{
+  args_info->since_when_arg     = kFSEventStreamEventIdSinceNow;
+  args_info->latency_arg        = 0.5;
+  args_info->no_defer_flag      = false;
+  args_info->watch_root_flag    = false;
+  args_info->ignore_self_flag   = false;
+  args_info->file_events_flag   = false;
+  args_info->mark_self_flag     = false;
+  args_info->format_arg         = 0;
+}
+
+static void cli_parser_release (struct cli_info* args_info)
+{
+  unsigned int i;
+
+  for (i=0; i < args_info->inputs_num; ++i) {
+    free(args_info->inputs[i]);
+  }
+
+  if (args_info->inputs_num) {
+    free(args_info->inputs);
+  }
+
+  args_info->inputs_num = 0;
+}
+
+void cli_parser_init (struct cli_info* args_info)
+{
+  default_args(args_info);
+
+  args_info->inputs = 0;
+  args_info->inputs_num = 0;
+}
+
+void cli_parser_free (struct cli_info* args_info)
+{
+  cli_parser_release(args_info);
+}
+
+static void cli_print_info_dict (const void *key,
+                                 const void *value,
+                                 void *context)
+{
+  CFStringRef entry = CFStringCreateWithFormat(NULL, NULL,
+    CFSTR("%@:\n  %@"), key, value);
+  if (entry) {
+    CFShow(entry);
+    CFRelease(entry);
+  }
+}
+
+void cli_show_plist (void)
+{
+  CFBundleRef mainBundle = CFBundleGetMainBundle();
+  CFRetain(mainBundle);
+  CFDictionaryRef mainBundleDict = CFBundleGetInfoDictionary(mainBundle);
+  if (mainBundleDict) {
+    CFRetain(mainBundleDict);
+    printf("Embedded Info.plist metadata:\n\n");
+    CFDictionaryApplyFunction(mainBundleDict, cli_print_info_dict, NULL);
+    CFRelease(mainBundleDict);
+  }
+  CFRelease(mainBundle);
+  printf("\n");
+}
+
+void cli_print_version (void)
+{
+  printf("%s %s\n\n", "VXZ", "1.0");
+}
+
+void cli_print_help (void)
+{
+  cli_print_version();
+
+  printf("\n%s\n", cli_info_purpose);
+  printf("\n%s\n", cli_info_usage);
+  printf("\n");
+
+  int i = 0;
+  while (cli_info_help[i]) {
+    printf("%s\n", cli_info_help[i++]);
+  }
+}
+
+int cli_parser (int argc, const char** argv, struct cli_info* args_info)
+{
+  static struct option longopts[] = {
+    { "help",         no_argument,        NULL, 'h' },
+    { "version",      no_argument,        NULL, 'V' },
+    { "show-plist",   no_argument,        NULL, 'p' },
+    { "since-when",   required_argument,  NULL, 's' },
+    { "latency",      required_argument,  NULL, 'l' },
+    { "no-defer",     no_argument,        NULL, 'n' },
+    { "watch-root",   no_argument,        NULL, 'r' },
+    { "ignore-self",  no_argument,        NULL, 'i' },
+    { "file-events",  no_argument,        NULL, 'F' },
+    { "mark-self",    no_argument,        NULL, 'm' },
+    { "format",       required_argument,  NULL, 'f' },
+    { 0, 0, 0, 0 }
+  };
+
+  const char* shortopts = "hVps:l:nriFf:";
+
+  int c = -1;
+
+  while ((c = getopt_long(argc, (char * const*)argv, shortopts, longopts, NULL)) != -1) {
+    switch(c) {
+    case 's': // since-when
+      args_info->since_when_arg = strtoull(optarg, NULL, 0);
+      break;
+    case 'l': // latency
+      args_info->latency_arg = strtod(optarg, NULL);
+      break;
+    case 'n': // no-defer
+      args_info->no_defer_flag = true;
+      break;
+    case 'r': // watch-root
+      args_info->watch_root_flag = true;
+      break;
+    case 'i': // ignore-self
+      args_info->ignore_self_flag = true;
+      break;
+    case 'F': // file-events
+      args_info->file_events_flag = true;
+      break;
+    case 'm': // mark-self
+      args_info->mark_self_flag = true;
+      break;
+    case 'f': // format
+      // XXX: ignored
+      break;
+    case 'V': // version
+      cli_print_version();
+      exit(EXIT_SUCCESS);
+    case 'p': // show-plist
+      cli_show_plist();
+      exit(EXIT_SUCCESS);
+    case 'h': // help
+    case '?': // invalid option
+    case ':': // missing argument
+      cli_print_help();
+      exit((c == 'h') ? EXIT_SUCCESS : EXIT_FAILURE);
+    }
+  }
+
+  if (optind < argc) {
+    int i = 0;
+    args_info->inputs_num = (unsigned int)(argc - optind);
+    args_info->inputs =
+      (char**)(malloc ((args_info->inputs_num)*sizeof(char*)));
+    while (optind < argc)
+      if (argv[optind++] != argv[0]) {
+        args_info->inputs[i++] = strdup(argv[optind-1]);
+      }
+  }
+
+  return EXIT_SUCCESS;
+}
+

+ 36 - 0
c_src/mac/cli.h

@@ -0,0 +1,36 @@
+#ifndef CLI_H
+#define CLI_H
+
+#include "common.h"
+
+#ifndef CLI_NAME
+#define CLI_NAME "fsevent_watch"
+#endif /* CLI_NAME */
+
+struct cli_info {
+  UInt64 since_when_arg;
+  double latency_arg;
+  bool no_defer_flag;
+  bool watch_root_flag;
+  bool ignore_self_flag;
+  bool file_events_flag;
+  bool mark_self_flag;
+  int format_arg;
+
+  char** inputs;
+  unsigned inputs_num;
+};
+
+extern const char* cli_info_purpose;
+extern const char* cli_info_usage;
+extern const char* cli_info_help[];
+
+void cli_print_help(void);
+void cli_print_version(void);
+
+int cli_parser (int argc, const char** argv, struct cli_info* args_info);
+void cli_parser_init (struct cli_info* args_info);
+void cli_parser_free (struct cli_info* args_info);
+
+
+#endif /* CLI_H */

+ 55 - 0
c_src/mac/common.h

@@ -0,0 +1,55 @@
+#ifndef fsevent_watch_common_h
+#define fsevent_watch_common_h
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreServices/CoreServices.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include "compat.h"
+
+#define _str(s) #s
+#define _xstr(s) _str(s)
+
+#define COMPILED_AT __DATE__ " " __TIME__
+
+#define FPRINTF_FLAG_CHECK(flags, flag, msg, fd)  \
+  do {                                            \
+    if ((flags) & (flag)) {                       \
+      fprintf(fd, "%s\n", msg); } }               \
+  while (0)
+
+#define FLAG_CHECK_STDERR(flags, flag, msg)       \
+        FPRINTF_FLAG_CHECK(flags, flag, msg, stderr)
+
+/*
+ * FSEVENTSBITS:
+ * generated by `make printflags` (and pasted here)
+ * flags MUST be ordered (bits ascending) and sorted
+ *
+ * idea from: http://www.openbsd.org/cgi-bin/cvsweb/src/sbin/ifconfig/ifconfig.c (see printb())
+ */
+#define	FSEVENTSBITS \
+"\1mustscansubdirs\2userdropped\3kerneldropped\4eventidswrapped\5historydone\6rootchanged\7mount\10unmount\11created\12removed\13inodemetamod\14renamed\15modified\16finderinfomod\17changeowner\20xattrmod\21isfile\22isdir\23issymlink\24ownevent"
+
+static inline void
+sprintb(char *buf, unsigned short v, char *bits)
+{
+        int i, any = 0;
+        char c;
+	char *bufp = buf;
+
+	while ((i = *bits++)) {
+		if (v & (1 << (i-1))) {
+			if (any)
+				*bufp++ = ',';
+			any = 1;
+			for (; (c = *bits) > 32; bits++)
+				*bufp++ = c;
+		} else
+			for (; *bits > 32; bits++)
+				;
+	}
+	*bufp = '\0';
+}
+
+#endif /* fsevent_watch_common_h */

+ 25 - 0
c_src/mac/compat.c

@@ -0,0 +1,25 @@
+#include "compat.h"
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
+FSEventStreamCreateFlags  kFSEventStreamCreateFlagIgnoreSelf        = 0x00000008;
+#endif
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070
+FSEventStreamCreateFlags  kFSEventStreamCreateFlagFileEvents        = 0x00000010;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemCreated        = 0x00000100;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemRemoved        = 0x00000200;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemInodeMetaMod   = 0x00000400;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemRenamed        = 0x00000800;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemModified       = 0x00001000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemFinderInfoMod  = 0x00002000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemChangeOwner    = 0x00004000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemXattrMod       = 0x00008000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemIsFile         = 0x00010000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemIsDir          = 0x00020000;
+FSEventStreamEventFlags   kFSEventStreamEventFlagItemIsSymlink      = 0x00040000;
+#endif
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090
+FSEventStreamCreateFlags  kFSEventStreamCreateFlagMarkSelf          = 0x00000020;
+FSEventStreamEventFlags   kFSEventStreamEventFlagOwnEvent           = 0x00080000;
+#endif

+ 47 - 0
c_src/mac/compat.h

@@ -0,0 +1,47 @@
+/**
+ * @headerfile compat.h
+ * FSEventStream flag compatibility shim
+ *
+ * In order to compile a binary against an older SDK yet still support the
+ * features present in later OS releases, we need to define any missing enum
+ * constants not present in the older SDK. This allows us to safely defer
+ * feature detection to runtime (and avoid recompilation).
+ */
+
+
+#ifndef fsevent_watch_compat_h
+#define fsevent_watch_compat_h
+
+#ifndef __CORESERVICES__
+#include <CoreServices/CoreServices.h>
+#endif // __CORESERVICES__
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
+// ignoring events originating from the current process introduced in 10.6
+extern FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf;
+#endif
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070
+// file-level events introduced in 10.7
+extern FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents;
+extern FSEventStreamEventFlags  kFSEventStreamEventFlagItemCreated,
+                                kFSEventStreamEventFlagItemRemoved,
+                                kFSEventStreamEventFlagItemInodeMetaMod,
+                                kFSEventStreamEventFlagItemRenamed,
+                                kFSEventStreamEventFlagItemModified,
+                                kFSEventStreamEventFlagItemFinderInfoMod,
+                                kFSEventStreamEventFlagItemChangeOwner,
+                                kFSEventStreamEventFlagItemXattrMod,
+                                kFSEventStreamEventFlagItemIsFile,
+                                kFSEventStreamEventFlagItemIsDir,
+                                kFSEventStreamEventFlagItemIsSymlink;
+#endif
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090
+// marking, rather than ignoring, events originating from the current process introduced in 10.9
+extern FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf;
+extern FSEventStreamEventFlags  kFSEventStreamEventFlagOwnEvent;
+#endif
+
+
+#endif // fsevent_watch_compat_h

+ 234 - 0
c_src/mac/main.c

@@ -0,0 +1,234 @@
+#include "common.h"
+#include "cli.h"
+
+// TODO: set on fire. cli.{h,c} handle both parsing and defaults, so there's
+//       no need to set those here. also, in order to scope metadata by path,
+//       each stream will need its own configuration... so this won't work as
+//       a global any more. In the end the goal is to make the output format
+//       able to declare not just that something happened and what flags were
+//       attached, but what path it was watching that caused those events (so
+//       that the path itself can be used for routing that information to the
+//       relevant callback).
+//
+// Structure for storing metadata parsed from the commandline
+static struct {
+  FSEventStreamEventId     sinceWhen;
+  CFTimeInterval           latency;
+  FSEventStreamCreateFlags flags;
+  CFMutableArrayRef        paths;
+  int                      format;
+} config = {
+  (UInt64) kFSEventStreamEventIdSinceNow,
+  (double) 0.3,
+  (CFOptionFlags) kFSEventStreamCreateFlagNone,
+  NULL,
+  0
+};
+
+// Prototypes
+static void         append_path(const char* path);
+static inline void  parse_cli_settings(int argc, const char* argv[]);
+static void         callback(FSEventStreamRef streamRef,
+                             void* clientCallBackInfo,
+                             size_t numEvents,
+                             void* eventPaths,
+                             const FSEventStreamEventFlags eventFlags[],
+                             const FSEventStreamEventId eventIds[]);
+
+
+static void append_path(const char* path)
+{
+  CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault,
+                                                  path,
+                                                  kCFStringEncodingUTF8);
+  CFArrayAppendValue(config.paths, pathRef);
+  CFRelease(pathRef);
+}
+
+// Parse commandline settings
+static inline void parse_cli_settings(int argc, const char* argv[])
+{
+  // runtime os version detection
+  SInt32 osMajorVersion, osMinorVersion;
+  if (!(Gestalt(gestaltSystemVersionMajor, &osMajorVersion) == noErr)) {
+    osMajorVersion = 0;
+  }
+  if (!(Gestalt(gestaltSystemVersionMinor, &osMinorVersion) == noErr)) {
+    osMinorVersion = 0;
+  }
+
+  if ((osMajorVersion == 10) & (osMinorVersion < 5)) {
+    fprintf(stderr, "The FSEvents API is unavailable on this version of macos!\n");
+    exit(EXIT_FAILURE);
+  }
+
+  struct cli_info args_info;
+  cli_parser_init(&args_info);
+
+  if (cli_parser(argc, argv, &args_info) != 0) {
+    exit(EXIT_FAILURE);
+  }
+
+  config.paths = CFArrayCreateMutable(NULL,
+                                      (CFIndex)0,
+                                      &kCFTypeArrayCallBacks);
+
+  config.sinceWhen = args_info.since_when_arg;
+  config.latency = args_info.latency_arg;
+  config.format = args_info.format_arg;
+
+  if (args_info.no_defer_flag) {
+    config.flags |= kFSEventStreamCreateFlagNoDefer;
+  }
+  if (args_info.watch_root_flag) {
+    config.flags |= kFSEventStreamCreateFlagWatchRoot;
+  }
+
+  if (args_info.ignore_self_flag) {
+    if ((osMajorVersion == 10) & (osMinorVersion >= 6)) {
+      config.flags |= kFSEventStreamCreateFlagIgnoreSelf;
+    } else {
+      fprintf(stderr, "MacOSX 10.6 or later is required for --ignore-self\n");
+      exit(EXIT_FAILURE);
+    }
+  }
+
+  if (args_info.file_events_flag) {
+    if ((osMajorVersion == 10) & (osMinorVersion >= 7)) {
+      config.flags |= kFSEventStreamCreateFlagFileEvents;
+    } else {
+      fprintf(stderr, "MacOSX 10.7 or later required for --file-events\n");
+      exit(EXIT_FAILURE);
+    }
+  }
+
+  if (args_info.mark_self_flag) {
+    if ((osMajorVersion == 10) & (osMinorVersion >= 9)) {
+      config.flags |= kFSEventStreamCreateFlagMarkSelf;
+    } else {
+      fprintf(stderr, "MacOSX 10.9 or later required for --mark-self\n");
+      exit(EXIT_FAILURE);
+    }
+  }
+
+  if (args_info.inputs_num == 0) {
+    append_path(".");
+  } else {
+    for (unsigned int i=0; i < args_info.inputs_num; ++i) {
+      append_path(args_info.inputs[i]);
+    }
+  }
+
+  cli_parser_free(&args_info);
+
+#ifdef DEBUG
+  fprintf(stderr, "config.sinceWhen    %llu\n", config.sinceWhen);
+  fprintf(stderr, "config.latency      %f\n", config.latency);
+  fprintf(stderr, "config.flags        %#.8x\n", config.flags);
+
+  FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagUseCFTypes,
+                    "  Using CF instead of C types");
+  FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagNoDefer,
+                    "  NoDefer latency modifier enabled");
+  FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagWatchRoot,
+                    "  WatchRoot notifications enabled");
+  FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagIgnoreSelf,
+                    "  IgnoreSelf enabled");
+  FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagFileEvents,
+                    "  FileEvents enabled");
+
+  fprintf(stderr, "config.paths\n");
+
+  long numpaths = CFArrayGetCount(config.paths);
+
+  for (long i = 0; i < numpaths; i++) {
+    char path[PATH_MAX];
+    CFStringGetCString(CFArrayGetValueAtIndex(config.paths, i),
+                       path,
+                       PATH_MAX,
+                       kCFStringEncodingUTF8);
+    fprintf(stderr, "  %s\n", path);
+  }
+
+  fprintf(stderr, "\n");
+#endif
+}
+
+static void callback(__attribute__((unused)) FSEventStreamRef streamRef,
+                     __attribute__((unused)) void* clientCallBackInfo,
+                     size_t numEvents,
+                     void* eventPaths,
+                     const FSEventStreamEventFlags eventFlags[],
+                     const FSEventStreamEventId eventIds[])
+{
+  char** paths = eventPaths;
+  char *buf = calloc(sizeof(FSEVENTSBITS), sizeof(char));
+
+  for (size_t i = 0; i < numEvents; i++) {
+    sprintb(buf, eventFlags[i], FSEVENTSBITS);
+    printf("%llu\t%#.8x=[%s]\t%s\n", eventIds[i], eventFlags[i], buf, paths[i]);
+  }
+  fflush(stdout);
+  free(buf);
+
+  if (fcntl(STDIN_FILENO, F_GETFD) == -1) {
+    CFRunLoopStop(CFRunLoopGetCurrent());
+  }
+}
+
+static void stdin_callback(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info)
+{
+  char buf[1024];
+  int nread;
+
+  do {
+    nread = read(STDIN_FILENO, buf, sizeof(buf));
+    if (nread == -1 && errno == EAGAIN) {
+      CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
+      return;
+    } else if (nread == 0) {
+      exit(1);
+      return;
+    }
+  } while (nread > 0);
+}
+
+int main(int argc, const char* argv[])
+{
+  parse_cli_settings(argc, argv);
+
+  FSEventStreamContext context = {0, NULL, NULL, NULL, NULL};
+  FSEventStreamRef stream;
+  stream = FSEventStreamCreate(kCFAllocatorDefault,
+                               (FSEventStreamCallback)&callback,
+                               &context,
+                               config.paths,
+                               config.sinceWhen,
+                               config.latency,
+                               config.flags);
+
+#ifdef DEBUG
+  FSEventStreamShow(stream);
+  fprintf(stderr, "\n");
+#endif
+
+  fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);
+
+  CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, false, stdin_callback, NULL);
+  CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
+  CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
+  CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
+  CFRelease(source);
+
+  FSEventStreamScheduleWithRunLoop(stream,
+                                   CFRunLoopGetCurrent(),
+                                   kCFRunLoopDefaultMode);
+  FSEventStreamStart(stream);
+  CFRunLoopRun();
+  FSEventStreamFlushSync(stream);
+  FSEventStreamStop(stream);
+
+  return 0;
+}
+
+// vim: ts=2 sts=2 et sw=2

+ 143 - 0
c_src/windows/ArgumentParser.cs

@@ -0,0 +1,143 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace De.Thekid.INotify
+{
+
+	/// See also <a href="http://linux.die.net/man/1/inotifywait">inotifywait(1) - Linux man page</a>
+	public class ArgumentParser
+	{
+
+		/// Helper method for parser
+		protected string Value(string[] args, int i, string name)
+		{
+			if (i > args.Length)
+			{
+				throw new ArgumentException("Argument " + name + " requires a value");
+			}
+			return args[i];
+		}
+
+		/// Tokenizes "printf" format string into an array of strings
+		protected string[] TokenizeFormat(string arg)
+		{
+			var result = new List<string>();
+			var tokens = arg.Split(new char[]{ '%' });
+			foreach (var token in tokens)
+			{
+				if (token.Length == 0) continue;
+
+				if ("efwT".IndexOf(token[0]) != -1)
+				{
+					result.Add(token[0].ToString());
+					if (token.Length > 1)
+					{
+						result.Add(token.Substring(1));
+					}
+				}
+				else
+				{
+					result.Add(token);
+				}
+			}
+			return result.ToArray();
+		}
+
+		private void ParseArgument(string option, string[] args, ref int i, Arguments result)
+		{
+			if ("--recursive" == option || "-r" == option)
+			{
+				result.Recursive = true;
+			}
+			else if ("--monitor" == option || "-m" == option)
+			{
+				result.Monitor = true;
+			}
+			else if ("--quiet" == option || "-q" == option)
+			{
+				result.Quiet = true;
+			}
+			else if ("--event" == option || "-e" == option)
+			{
+				result.Events = new List<string>(Value(args, ++i, "event").Split(','));
+			}
+			else if ("--format" == option)
+			{
+				result.Format = TokenizeFormat(Value(args, ++i, "format"));
+			}
+			else if ("--exclude" == option)
+			{
+				result.Exclude = new Regex(Value(args, ++i, "exclude"));
+			}
+			else if ("--excludei" == option)
+			{
+				result.Exclude = new Regex(Value(args, ++i, "exclude"), RegexOptions.IgnoreCase);
+			}
+			else if (option.StartsWith("--event="))
+			{
+				result.Events = new List<string>(option.Split(new Char[]{'='}, 2)[1].Split(','));
+			}
+			else if (option.StartsWith("--format="))
+			{
+				result.Format = TokenizeFormat(option.Split(new Char[]{'='}, 2)[1]);
+			}
+			else if (option.StartsWith("--exclude="))
+			{
+				result.Exclude = new Regex(option.Split(new Char[]{'='}, 2)[1]);
+			}
+			else if (option.StartsWith("--excludei="))
+			{
+				result.Exclude = new Regex(option.Split(new Char[]{'='}, 2)[1], RegexOptions.IgnoreCase);
+			}
+			else if (Directory.Exists(option))
+			{
+				result.Paths.Add(System.IO.Path.GetFullPath(option));
+			}
+		}
+
+		/// Creates a new argument parser and parses the arguments
+		public Arguments Parse(string[] args)
+		{
+			var result = new Arguments();
+			for (var i = 0; i < args.Length; i++)
+			{
+				if (!args[i].StartsWith("--") && args[i].StartsWith("-") && args[i].Length > 2)
+				{
+					string options = args[i];
+					for (var j = 1; j < options.Length; j++)
+					{
+						ParseArgument("-" + options.Substring(j, 1), args, ref i, result);
+					}
+				}
+				else
+				{
+					ParseArgument(args[i], args, ref i, result);
+				}
+			}
+			return result;
+		}
+
+		/// Usage
+		public void PrintUsage(string name, TextWriter writer)
+		{
+			writer.WriteLine("Usage: " + name + " [options] path [...]");
+			writer.WriteLine();
+			writer.WriteLine("Options:");
+			writer.WriteLine("-r/--recursive:  Recursively watch all files and subdirectories inside path");
+			writer.WriteLine("-m/--monitor:    Keep running until killed (e.g. via Ctrl+C)");
+			writer.WriteLine("-q/--quiet:      Do not output information about actions");
+			writer.WriteLine("-e/--event list: Which events (create, modify, delete, move) to watch, comma-separated. Default: all");
+			writer.WriteLine("--format format: Format string for output.");
+			writer.WriteLine("--exclude:       Do not process any events whose filename matches the specified regex");
+			writer.WriteLine("--excludei:      Ditto, case-insensitive");
+			writer.WriteLine();
+			writer.WriteLine("Formats:");
+			writer.WriteLine("%e             : Event name");
+			writer.WriteLine("%f             : File name");
+			writer.WriteLine("%w             : Path name");
+			writer.WriteLine("%T             : Current date and time");
+		}
+	}
+}

+ 32 - 0
c_src/windows/Arguments.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace De.Thekid.INotify
+{
+
+	public class Arguments
+	{
+		// Default values
+		private List<string> _Events = new List<string>(new string[] { "create", "modify", "delete", "move" });
+		private string[] _Format = new string[] { "w", " ", "e", " ", "f" };
+		private List<string> _Paths = new List<string>();
+
+		public bool Recursive { get; set; }
+		public bool Monitor { get; set; }
+		public bool Quiet { get; set; }
+		public List<string> Paths {
+			get { return this._Paths; }
+		}
+		public string[] Format {
+			get { return this._Format; }
+			set { this._Format = value; }
+		}
+		public List<string> Events
+		{
+			get { return this._Events; }
+			set { this._Events = value; }
+		}
+		public Regex Exclude { get; set; }
+	}
+}

+ 16 - 0
c_src/windows/AssemblyInfo.cs

@@ -0,0 +1,16 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+
+[assembly: AssemblyTitle("https://github.com/thekid/inotify-win")]
+[assembly: AssemblyDescription("A port of the inotifywait tool for Windows")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Timm Friebe")]
+[assembly: AssemblyProduct("inotify-win")]
+[assembly: AssemblyCopyright("Copyright © 2012 - 2015 Timm Friebe")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: AssemblyVersion("1.5.1.0")]
+[assembly: ComVisible(false)]
+[assembly: Guid("4254314b-ae21-4e2f-ba52-d6f3d83a86b5")]

+ 15 - 0
c_src/windows/Makefile

@@ -0,0 +1,15 @@
+ifeq ($(OS),Windows_NT)
+        BASE=$(shell cd "$(WINDIR)";pwd -W)
+        CSC?=$(shell ls -1d $(BASE)/Microsoft.NET/Framework/v*|sort -rn|head -1)/csc.exe
+else
+        CSC?=csc
+endif
+
+MKDIR_P = mkdir -p
+
+inotifywait.exe: c_src/windows/*.cs
+	${MKDIR_P} priv
+	$(CSC) //nologo //target:exe //out:priv\\$@ c_src\\windows\\*.cs
+
+clean:
+	-rm priv\\inotifywait.exe

+ 209 - 0
c_src/windows/Runner.cs

@@ -0,0 +1,209 @@
+using System;
+using System.Threading;
+using System.IO;
+using System.Collections.Generic;
+
+namespace De.Thekid.INotify
+{
+	// List of possible changes
+	public enum Change
+	{
+		CREATE, MODIFY, DELETE, MOVED_FROM, MOVED_TO
+	}
+
+	/// Main class
+	public class Runner
+	{
+		// Mappings
+		protected static Dictionary<WatcherChangeTypes, Change> Changes = new Dictionary<WatcherChangeTypes, Change>();
+
+		private List<Thread> _threads = new List<Thread>();
+		private bool _stopMonitoring = false;
+		private ManualResetEventSlim _stopMonitoringEvent;
+		private object _notificationReactionLock = new object();
+		private Arguments _args = null;
+
+		static Runner()
+		{
+			Changes[WatcherChangeTypes.Created]= Change.CREATE;
+			Changes[WatcherChangeTypes.Changed]= Change.MODIFY;
+			Changes[WatcherChangeTypes.Deleted]= Change.DELETE;
+		}
+
+		public Runner(Arguments args)
+		{
+			_args = args;
+		}
+
+		/// Callback for errors in watcher
+		protected void OnWatcherError(object source, ErrorEventArgs e)
+		{
+			Console.Error.WriteLine("*** {0}", e.GetException());
+		}
+
+		private void OnWatcherNotification(object sender, FileSystemEventArgs e)
+		{
+		    FileSystemWatcher w = (FileSystemWatcher)sender;
+		    HandleNotification(w, e, () => Output(Console.Out, _args.Format, w, Changes[e.ChangeType], e.Name));
+		}
+		
+		private void OnRenameNotification(object sender, RenamedEventArgs e)
+		{
+		    FileSystemWatcher w = (FileSystemWatcher)sender;
+		    HandleNotification(w, e, () =>
+		    {
+		        Output(Console.Out, _args.Format, w, Change.MOVED_FROM, e.OldName);
+		        Output(Console.Out, _args.Format, w, Change.MOVED_TO, e.Name);
+		    });
+		}
+		
+		private void HandleNotification(FileSystemWatcher sender, FileSystemEventArgs e, Action outputAction)
+		{
+		    FileSystemWatcher w = (FileSystemWatcher)sender;
+		    // Lock so we don't output more than one change if we were only supposed to watch for one.
+		    // And to serialize access to the console
+		    lock (_notificationReactionLock)
+		    {
+		        // if only looking for one change and another thread beat us to it, return
+		        if (!_args.Monitor && _stopMonitoring)
+		        {
+		            return;
+		        }
+		
+		        if (null != _args.Exclude && _args.Exclude.IsMatch(e.FullPath))
+		        {
+		            return;
+		        }
+		
+		        outputAction();
+		
+		        // If only looking for one change, signal to stop
+		        if (!_args.Monitor)
+		        {
+		            _stopMonitoring = true;
+		            _stopMonitoringEvent.Set();
+		        }
+		    }
+		}
+
+		/// Output method
+		protected void Output(TextWriter writer, string[] tokens, FileSystemWatcher source, Change type, string name)
+		{
+			foreach (var token in tokens)
+			{
+				var path = Path.Combine(source.Path, name);
+				switch (token[0])
+				{
+					case 'e':
+						writer.Write(type);
+						if (Directory.Exists(path))
+						{
+							writer.Write(",ISDIR");
+						}
+						break;
+					case 'f': writer.Write(Path.GetFileName(path)); break;
+					case 'w': writer.Write(Path.Combine(source.Path, Path.GetDirectoryName(path))); break;
+					case 'T': writer.Write(DateTime.Now); break;
+					default: writer.Write(token); break;
+				}
+			}
+			writer.WriteLine();
+		}
+
+                public void Stop(object data) {
+                  string s = Console.ReadLine();
+		  _stopMonitoring = true;
+		  _stopMonitoringEvent.Set();
+                                   
+                }
+
+		public void Processor(object data) {
+			string path = (string)data;
+			using (var w = new FileSystemWatcher {
+				Path = path,
+				IncludeSubdirectories = _args.Recursive,
+				Filter = "*.*"
+			}) {
+				w.Error += new ErrorEventHandler(OnWatcherError);
+
+				// Parse "events" argument
+				WatcherChangeTypes changes = 0;
+				if (_args.Events.Contains("create"))
+				{
+					changes |= WatcherChangeTypes.Created;
+					w.Created += new FileSystemEventHandler(OnWatcherNotification);
+				}
+				if (_args.Events.Contains("modify"))
+				{
+					changes |= WatcherChangeTypes.Changed;
+					w.Changed += new FileSystemEventHandler(OnWatcherNotification);
+				}
+				if (_args.Events.Contains("delete"))
+				{
+					changes |= WatcherChangeTypes.Deleted;
+					w.Deleted += new FileSystemEventHandler(OnWatcherNotification);
+				}
+				if (_args.Events.Contains("move"))
+				{
+					changes |= WatcherChangeTypes.Renamed;
+					w.Renamed += new RenamedEventHandler(OnRenameNotification);
+				}
+
+				// Main loop
+				if (!_args.Quiet)
+				{
+					Console.Error.WriteLine(
+						"===> {0} for {1} in {2}{3} for {4}",
+						_args.Monitor ? "Monitoring" : "Watching",
+						changes,
+						path,
+						_args.Recursive ? " -r" : "",
+						String.Join(", ", _args.Events.ToArray())
+					);
+				}
+				w.EnableRaisingEvents = true;
+				_stopMonitoringEvent.Wait();
+			}
+		}
+
+		/// Entry point
+		public int Run()
+		{
+			using (_stopMonitoringEvent = new ManualResetEventSlim(initialState: false))
+			{
+			    foreach (var path in _args.Paths)
+			    {
+			        Thread t = new Thread(new ParameterizedThreadStart(Processor));
+			        t.Start(path);
+			        _threads.Add(t);
+			    }
+                            Thread stop = new Thread(new ParameterizedThreadStart(Stop));
+                            stop.Start("");
+			    _stopMonitoringEvent.Wait();
+			    foreach (var thread in _threads)
+			    {
+			        if (thread.IsAlive)
+			            thread.Abort();
+			        thread.Join();
+			    }
+			    return 0;
+			}
+		}
+
+		/// Entry point method
+		public static int Main(string[] args)
+		{
+			var p = new ArgumentParser();
+
+			// Show usage if no args or standard "help" args are given
+			if (0 == args.Length || args[0].Equals("-?") || args[0].Equals("--help"))
+			{
+				p.PrintUsage("inotifywait", Console.Error);
+				return 1;
+			}
+
+			// Run!
+			return new Runner(p.Parse(args)).Run();
+		}
+	}
+}

+ 1 - 0
include/api.hrl

@@ -0,0 +1 @@
+-define(API, [find_executable/0, start_port/2, known_events/0, line_to_event/1]).

+ 16 - 0
package.exs

@@ -0,0 +1,16 @@
+defmodule FS.Mixfile do
+  use Mix.Project
+
+  def project do
+    [app: :fs,
+     version: "1.9",
+     description: "Erlang FileSystem Listener",
+     package: package]
+  end
+
+  defp package do
+    [files: ~w(c_src include priv src LICENSE package.exs README.md rebar.config),
+     licenses: ["MIT"],
+     links: %{"GitHub" => "https://github.com/synrc/fs"}]
+   end
+end

BIN
priv/inotifywait.exe


+ 19 - 0
rebar.config

@@ -0,0 +1,19 @@
+{port_env,
+ [{"darwin", "LDFLAGS", "-framework CoreFoundation -framework CoreServices"},
+  {"darwin", "CC", "clang"},
+  {"darwin", "CFLAGS", "-Wno-deprecated-declarations"},
+  {"freebsd", "LDFLAGS", ""},
+  {"freebsd", "CC", "cc"},
+  {"freebsd", "CFLAGS", ""}]
+}.
+
+{port_specs,
+ [{"darwin", "priv/mac_listener", ["c_src/mac/*.c"]},
+  {"freebsd", "priv/kqueue", ["c_src/bsd/*.c"]}
+  ]}.
+
+%{pre_hooks, [
+%             {"win32",  compile, "make -f c_src/windows/Makefile"},
+%             {"win32",  clean,   "make -f c_src/windows/Makefile clean"}
+%            ]
+%}.

+ 7 - 0
src/fs.app.src

@@ -0,0 +1,7 @@
+{application, fs,
+ [{description, "FS VXZ Listener"},
+  {vsn, "1.9"},
+  {registered, []},
+  {applications, [kernel,stdlib]},
+  {mod, { fs_app, []}},
+  {env, []}]}.

+ 48 - 0
src/fs.erl

@@ -0,0 +1,48 @@
+-module(fs).
+-include_lib("kernel/include/file.hrl").
+-export([subscribe/0, known_events/0, start_looper/0, path/0, find_executable/2]).
+
+% sample subscriber
+
+subscribe() -> gen_event:add_sup_handler(fs_events, {fs_event_bridge, self()}, [self()]).
+known_events() -> gen_server:call(fs_server, known_events).
+start_looper() -> spawn(fun() -> subscribe(), loop() end).
+
+path() ->
+    case application:get_env(fs, path) of
+        {ok, P} -> filename:absname(P);
+        undefined -> filename:absname("") end.
+
+loop() ->
+    receive
+        {_Pid, {fs, file_event}, {Path, Flags}} -> error_logger:info_msg("file_event: ~p ~p", [Path, Flags]);
+        _ -> ignore end,
+    loop().
+
+find_executable(Cmd, DepsPath) ->
+    case priv_file(Cmd) of
+    false -> mad_file(DepsPath);
+    Priv  -> Priv end.
+
+mad_file(DepsPath) ->
+    case filelib:is_regular(DepsPath) of
+    true  -> DepsPath;
+    false ->
+        case mad_repl:load_file(DepsPath) of
+        {error,_} ->
+            %% This path has been already checked in find_executable/2
+            false;
+        {ok,ETSFile} ->
+            filelib:ensure_dir(DepsPath),
+            file:write_file(DepsPath, ETSFile),
+            file:write_file_info(DepsPath, #file_info{mode=8#00555}) end end.
+
+priv_file(Cmd) ->
+    case code:priv_dir(fs) of
+    Priv when is_list(Priv) ->
+        Path = filename:join(Priv, Cmd),
+        case filelib:is_regular(Path) of
+        true  -> Path;
+        false -> false end;
+    _ ->
+        false end.

+ 6 - 0
src/fs_app.erl

@@ -0,0 +1,6 @@
+-module(fs_app).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) -> fs_sup:start_link().
+stop(_State) -> ok.

+ 10 - 0
src/fs_event_bridge.erl

@@ -0,0 +1,10 @@
+-module(fs_event_bridge).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, terminate/2]).
+
+init([Pid]) -> {ok, Pid}.
+handle_event(Event, Pid) -> Pid ! Event, {ok, Pid}.
+handle_call(_, State) -> {ok, ok, State}.
+handle_info(_, State) -> {ok, State}.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+terminate(_Reason, _State) -> ok.

+ 26 - 0
src/fs_server.erl

@@ -0,0 +1,26 @@
+-module(fs_server).
+-behaviour(gen_server).
+-define(SERVER, ?MODULE).
+-export([start_link/3]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,terminate/2, code_change/3]).
+
+-record(state, {port, path, backend}).
+
+notify(file_event = A, Msg) -> Key = {fs, A}, gen_event:notify(fs_events, {self(), Key, Msg}).
+start_link(Backend, Path, Cwd) -> gen_server:start_link({local, ?SERVER}, ?MODULE, [Backend, Path, Cwd], []).
+init([Backend, Path, Cwd]) -> {ok, #state{port=Backend:start_port(Path, Cwd),path=Path,backend=Backend}}.
+
+handle_call(known_events, _From, #state{backend=Backend} = State) -> {reply, Backend:known_events(), State};
+handle_call(_Request, _From, State) -> {reply, ok, State}.
+handle_cast(_Msg, State) -> {noreply, State}.
+handle_info({_Port, {data, {eol, Line}}}, #state{backend=Backend} = State) ->
+    Event = Backend:line_to_event(Line),
+    notify(file_event, Event),
+    {noreply, State};
+handle_info({_Port, {data, {noeol, Line}}}, State) ->
+    error_logger:error_msg("~p line too long: ~p, ignoring~n", [?SERVER, Line]),
+    {noreply, State};
+handle_info({_Port, {exit_status, Status}}, State) -> {stop, {port_exit, Status}, State};
+handle_info(_Info, State) -> {noreply, State}.
+terminate(_Reason, #state{port=Port}) -> (catch port_close(Port)), ok.
+code_change(_OldVsn, State, _Extra) -> {ok, State}.

+ 34 - 0
src/fs_sup.erl

@@ -0,0 +1,34 @@
+-module(fs_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+-define(CHILD(I, Type, Args), {I, {I, start_link, Args}, permanent, 5000, Type, [I]}).
+
+start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+init([]) ->
+    Backend = case os:type() of
+        {unix, darwin} -> fsevents;
+        {unix, linux} -> inotifywait;
+        {unix, _} -> kqueue;
+        {win32, nt} -> inotifywait_win32;
+        _ -> undefined end,
+
+    Children = case has_executable(Backend) of
+        false -> [];
+        true  -> Path = fs:path(), [?CHILD(fs_server, worker, [Backend, Path, Path])] end,
+
+    {ok, { {one_for_one, 5, 10},
+        Children ++ [?CHILD(gen_event, worker, [{local, fs_events}])]} }.
+
+has_executable(undefined) ->
+    os_not_supported(), false;
+has_executable(Backend) ->
+    case Backend:find_executable() of
+        false -> backend_port_not_found(Backend), false;
+        _     -> true end.
+
+os_not_supported() ->
+    error_logger:error_msg("fs does not support the current operating system~n",[]).
+
+backend_port_not_found(Backend) ->
+    error_logger:error_msg("backend port not found: ~p~n",[Backend]).

+ 22 - 0
src/sys/fsevents.erl

@@ -0,0 +1,22 @@
+-module(fsevents).
+-include("api.hrl").
+-export(?API).
+
+find_executable() ->
+    fs:find_executable("mac_listener", "deps/fs/priv/mac_listener").
+
+known_events() ->
+    [mustscansubdirs,userdropped,kerneldropped,eventidswrapped,historydone,rootchanged,
+        mount,unmount,created,removed,inodemetamod,renamed,modified,finderinfomod,changeowner,
+        xattrmod,isfile,isdir,issymlink,ownevent].
+
+start_port(Path, Cwd) ->
+    erlang:open_port({spawn_executable, find_executable()},
+        [stream, exit_status, {line, 16384}, {args, ["-F", Path]}, {cd, Cwd}]).
+
+line_to_event(Line) ->
+    [_EventId, Flags1, Path] = string:tokens(Line, [$\t]),
+    [_, Flags2] = string:tokens(Flags1, [$=]),
+    {ok, T, _} = erl_scan:string(Flags2 ++ "."),
+    {ok, Flags} = erl_parse:parse_term(T),
+    {Path, Flags}.

+ 37 - 0
src/sys/inotifywait.erl

@@ -0,0 +1,37 @@
+-module(inotifywait).
+-include("api.hrl").
+-export(?API).
+
+find_executable() -> os:find_executable("inotifywait").
+known_events() -> [created, deleted, renamed, closed, modified, isdir, undefined].
+
+start_port(Path, Cwd) ->
+    Path1 = filename:absname(Path),
+    Args = ["-c", "inotifywait $0 $@ & PID=$!; read a; kill $PID",
+            "-m", "-e", "close_write", "-e", "moved_to", "-e", "create", "-e", "delete", "-r", Path1],
+    erlang:open_port({spawn_executable, os:find_executable("sh")},
+        [stream, exit_status, {line, 16384}, {args, Args}, {cd, Cwd}]).
+
+line_to_event(Line) ->
+    {match, [Dir, Flags1, DirEntry]} = re:run(Line, re(), [{capture, all_but_first, list}]),
+    Flags = [convert_flag(F) || F <- string:tokens(Flags1, ",")],
+    Path = Dir ++ DirEntry,
+    {Path, Flags}.
+
+convert_flag("CREATE") -> created;
+convert_flag("DELETE") -> deleted;
+convert_flag("ISDIR") -> isdir;
+convert_flag("CLOSE_WRITE") -> modified;
+convert_flag("CLOSE") -> closed;
+convert_flag("MOVED_TO") -> renamed;
+convert_flag(_) -> undefined.
+
+re() ->
+    case get(inotifywait_re) of
+        undefined ->
+            {ok, R} = re:compile("^(.*/) ([A-Z_,]+) (.*)$", [unicode]),
+            put(inotifywait_re, R),
+            R;
+        V -> V
+    end.
+

+ 37 - 0
src/sys/inotifywait_win32.erl

@@ -0,0 +1,37 @@
+-module(inotifywait_win32).
+-include("api.hrl").
+-export(?API).
+
+find_executable() ->
+    fs:find_executable("inotifywait.exe", "deps/fs/priv/inotifywait.exe").
+
+known_events() -> [created, modified, removed, renamed, undefined].
+
+start_port(Path, Cwd) ->
+    Path1 = filename:absname(Path),
+    Args = ["-m", "-r", Path1],
+    erlang:open_port({spawn_executable, find_executable()},
+        [stream, exit_status, {line, 16384}, {args, Args}, {cd, Cwd}]).
+
+line_to_event(Line) ->
+    {match, [Dir, Flags1, DirEntry]} = re:run(Line, re(), [{capture, all_but_first, list}]),
+    Flags = [convert_flag(F) || F <- string:tokens(Flags1, ",")],
+    Path = filename:join(Dir,DirEntry),
+    {Path, Flags}.
+
+
+convert_flag("CREATE") -> created;
+convert_flag("MODIFY") -> modified;
+convert_flag("DELETE") -> removed;
+convert_flag("MOVE") -> renamed;
+convert_flag(_) -> undefined.
+
+re() ->
+    case get(inotifywait_re) of
+        undefined ->
+            {ok, R} = re:compile("^(.*\\\\.*) ([A-Z_,]+) (.*)$", [unicode]),
+            put(inotifywait_re, R),
+            R;
+        V -> V
+    end.
+

+ 12 - 0
src/sys/kqueue.erl

@@ -0,0 +1,12 @@
+-module(kqueue).
+-include("api.hrl").
+-export(?API).
+
+known_events() -> [created, deleted, renamed, closed, modified, isdir, undefined].
+line_to_event(Line) ->
+    io:format("Line: ~p~n",[Line]),
+    {".",Line}.
+find_executable() -> fs:find_executable("kqueue", "deps/fs/priv/kqueue").
+start_port(Path, Cwd) ->
+    erlang:open_port({spawn_executable, find_executable()},
+        [stream, exit_status, {line, 16384}, {args, Path}, {cd, Cwd}]).