#include #include #include #include // Structure for storing metadata parsed from the commandline static struct { FSEventStreamEventId sinceWhen; CFTimeInterval latency; FSEventStreamCreateFlags flags; CFMutableArrayRef paths; } config = { (UInt64) kFSEventStreamEventIdSinceNow, (double) 0.3, (UInt32) kFSEventStreamCreateFlagNone, NULL }; // 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[]); // Resolve a path and append it to the CLI settings structure // The FSEvents API will, internally, resolve paths using a similar scheme. // Performing this ahead of time makes things less confusing, IMHO. static void append_path(const char *path) { #ifdef DEBUG fprintf(stderr, "\n"); fprintf(stderr, "append_path called for: %s\n", path); #endif char fullPath[PATH_MAX]; if (realpath(path, fullPath) == NULL) { #ifdef DEBUG fprintf(stderr, " realpath not directly resolvable from path\n"); #endif if (path[0] != '/') { #ifdef DEBUG fprintf(stderr, " passed path is not absolute\n"); #endif size_t len; getcwd(fullPath, sizeof(fullPath)); #ifdef DEBUG fprintf(stderr, " result of getcwd: %s\n", fullPath); #endif len = strlen(fullPath); fullPath[len] = '/'; strlcpy(&fullPath[len + 1], path, sizeof(fullPath) - (len + 1)); } else { #ifdef DEBUG fprintf(stderr, " assuming path does not YET exist\n"); #endif strlcpy(fullPath, path, sizeof(fullPath)); } } #ifdef DEBUG fprintf(stderr, " resolved path to: %s\n", fullPath); fprintf(stderr, "\n"); fflush(stderr); #endif CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault, fullPath, kCFStringEncodingUTF8); CFArrayAppendValue(config.paths, pathRef); CFRelease(pathRef); } // Parse commandline settings static inline void parse_cli_settings(int argc, const char *argv[]) { config.paths = CFArrayCreateMutable(NULL, (CFIndex)0, &kCFTypeArrayCallBacks); for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--since-when") == 0) { config.sinceWhen = strtoull(argv[++i], NULL, 0); } else if (strcmp(argv[i], "--latency") == 0) { config.latency = strtod(argv[++i], NULL); } else if (strcmp(argv[i], "--no-defer") == 0) { config.flags |= kFSEventStreamCreateFlagNoDefer; } else if (strcmp(argv[i], "--watch-root") == 0) { config.flags |= kFSEventStreamCreateFlagWatchRoot; } else if (strcmp(argv[i], "--ignore-self") == 0) { #if MAC_OS_X_VERSION_MAX_ALLOWED >= 1060 config.flags |= kFSEventStreamCreateFlagIgnoreSelf; #else fprintf(stderr, "MacOSX10.6.sdk is required for --ignore-self\n"); #endif } else { append_path(argv[i]); } } if (CFArrayGetCount(config.paths) == 0) { append_path("."); } #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); 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"); fflush(stderr); #endif } static void callback(FSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { char **paths = eventPaths; #ifdef DEBUG fprintf(stderr, "\n"); fprintf(stderr, "FSEventStreamCallback fired!\n"); fprintf(stderr, " numEvents: %lu\n", numEvents); for (size_t i = 0; i < numEvents; i++) { fprintf(stderr, " event path: %s\n", paths[i]); fprintf(stderr, " event flags: %#.8x\n", eventFlags[i]); fprintf(stderr, " event ID: %llu\n", eventIds[i]); } fprintf(stderr, "\n"); fflush(stderr); #endif for (size_t i = 0; i < numEvents; i++) { fprintf(stdout, "%s", paths[i]); fprintf(stdout, ":"); } fprintf(stdout, "\n"); fflush(stdout); } int main(int argc, const char *argv[]) { /* * a subprocess will initially inherit the process group of its parent. the * process group may have a control terminal associated with it, which would * be the first tty device opened by the group leader. typically the group * leader is your shell and the control terminal is your login device. a * subset of signals triggered on the control terminal are sent to all members * of the process group, in large part to facilitate sane and consistent * cleanup (ex: control terminal was closed). * * so why the overly descriptive lecture style comment? * 1. SIGINT and SIGQUIT are among the signals with this behavior * 2. a number of applications gank the above for their own use * 3. ruby's insanely useful "guard" is one of these applications * 4. despite having some level of understanding of POSIX signals and a few * of the scenarios that might cause problems, i learned this one only * after reading ruby 1.9's process.c * 5. if left completely undocumented, even slightly obscure bugfixes * may be removed as cruft by a future maintainer * * hindsight is 20/20 addition: if you're single-threaded and blocking on IO * with a subprocess, then handlers for deferrable signals might not get run * when you expect them to. In the case of Ruby 1.8, that means making use of * IO::select, which will preserve correct signal handling behavior. */ if (setpgid(0,0) < 0) { fprintf(stderr, "Unable to set new process group.\n"); return 1; } 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"); fflush(stderr); #endif FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(stream); CFRunLoopRun(); FSEventStreamFlushSync(stream); FSEventStreamStop(stream); return 0; }