blob: b18596a60345cee63db868f8f1cb4cefd6203f66 [file] [log] [blame]
#include "common.h"
#include "signal_handlers.h"
#include "cli.h"
#include "FSEventsFix.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;
enum FSEventWatchOutputFormat format;
} config = {
(UInt64) kFSEventStreamEventIdSinceNow,
(double) 0.3,
(CFOptionFlags) kFSEventStreamCreateFlagNone,
NULL,
kFSEventWatchOutputFormatOTNetstring
};
// 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 bool needs_fsevents_fix = false;
// 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
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
#ifdef DEBUG
fprintf(stderr, "compiled against 10.6+, using CFURLCreateFileReferenceURL\n");
#endif
CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8*)path, (CFIndex)strlen(path), false);
CFURLRef placeholder = CFURLCopyAbsoluteURL(url);
CFRelease(url);
CFMutableArrayRef imaginary = NULL;
// if we don't have an existing url, spin until we get to a parent that
// does exist, saving any imaginary components for appending back later
while(!CFURLResourceIsReachable(placeholder, NULL)) {
#ifdef DEBUG
fprintf(stderr, "path does not exist\n");
#endif
CFStringRef child;
if (imaginary == NULL) {
imaginary = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
}
child = CFURLCopyLastPathComponent(placeholder);
CFArrayInsertValueAtIndex(imaginary, 0, child);
CFRelease(child);
url = CFURLCreateCopyDeletingLastPathComponent(NULL, placeholder);
CFRelease(placeholder);
placeholder = url;
#ifdef DEBUG
fprintf(stderr, "parent: ");
CFShow(placeholder);
#endif
}
#ifdef DEBUG
fprintf(stderr, "path exists\n");
#endif
// realpath() doesn't always return the correct case for a path, so this
// is a funky workaround that converts a path into a (volId/inodeId) pair
// and asks what the path should be for that. since it looks at the actual
// inode instead of returning the same case passed in like realpath()
// appears to do for HFS+, it should always be correct.
url = CFURLCreateFileReferenceURL(NULL, placeholder, NULL);
CFRelease(placeholder);
placeholder = CFURLCreateFilePathURL(NULL, url, NULL);
CFRelease(url);
#ifdef DEBUG
fprintf(stderr, "path resolved to: ");
CFShow(placeholder);
#endif
// if we stripped off any imaginary path components, append them back on
if (imaginary != NULL) {
CFIndex count = CFArrayGetCount(imaginary);
for (CFIndex i = 0; i<count; i++) {
CFStringRef component = CFArrayGetValueAtIndex(imaginary, i);
#ifdef DEBUG
fprintf(stderr, "appending component: ");
CFShow(component);
#endif
url = CFURLCreateCopyAppendingPathComponent(NULL, placeholder, component, false);
CFRelease(placeholder);
placeholder = url;
}
CFRelease(imaginary);
}
#ifdef DEBUG
fprintf(stderr, "result: ");
CFShow(placeholder);
#endif
CFStringRef cfPath = CFURLCopyFileSystemPath(placeholder, kCFURLPOSIXPathStyle);
CFRelease(placeholder);
char cPath[PATH_MAX];
if (CFStringGetCString(cfPath, cPath, PATH_MAX, kCFStringEncodingUTF8)) {
FSEventsFixRepairStatus status = FSEventsFixRepairIfNeeded(cPath);
if (status == FSEventsFixRepairStatusFailed) {
needs_fsevents_fix = true;
}
}
CFArrayAppendValue(config.paths, cfPath);
CFRelease(cfPath);
#else
#ifdef DEBUG
fprintf(stderr, "compiled against 10.5, using realpath()\n");
#endif
char fullPath[PATH_MAX + 1];
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");
#endif
CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault,
fullPath,
kCFStringEncodingUTF8);
CFArrayAppendValue(config.paths, pathRef);
CFRelease(pathRef);
#endif
}
// 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);
// STFU clang
#if defined(__LP64__)
fprintf(stderr, "config.flags %#.8x\n", config.flags);
#else
fprintf(stderr, "config.flags %#.8lx\n", config.flags);
#endif
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
}
// original output format for rb-fsevent
static void classic_output_format(size_t numEvents,
char** paths)
{
for (size_t i = 0; i < numEvents; i++) {
fprintf(stdout, "%s:", paths[i]);
}
fprintf(stdout, "\n");
}
// output format used in the Yoshimasa Niwa branch of rb-fsevent
static void niw_output_format(size_t numEvents,
char** paths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
for (size_t i = 0; i < numEvents; i++) {
fprintf(stdout, "%lu:%llu:%s\n",
(unsigned long)eventFlags[i],
(unsigned long long)eventIds[i],
paths[i]);
}
fprintf(stdout, "\n");
}
static void tstring_output_format(size_t numEvents,
char** paths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[],
TSITStringFormat format)
{
CFMutableArrayRef events = CFArrayCreateMutable(kCFAllocatorDefault,
0, &kCFTypeArrayCallBacks);
for (size_t i = 0; i < numEvents; i++) {
CFMutableDictionaryRef event = CFDictionaryCreateMutable(kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFStringRef path = CFStringCreateWithBytes(kCFAllocatorDefault,
(const UInt8*)paths[i],
(CFIndex)strlen(paths[i]),
kCFStringEncodingUTF8,
false);
CFDictionarySetValue(event, CFSTR("path"), path);
CFNumberRef ident = CFNumberCreate(kCFAllocatorDefault, kCFNumberLongLongType, &eventIds[i]);
CFDictionarySetValue(event, CFSTR("id"), ident);
CFNumberRef cflags = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &eventFlags[i]);
CFDictionarySetValue(event, CFSTR("cflags"), cflags);
CFMutableArrayRef flags = CFArrayCreateMutable(kCFAllocatorDefault,
0, &kCFTypeArrayCallBacks);
#define FLAG_ADD_NAME(flagsnum, flagnum, flagname, flagarray) \
do { \
if (FLAG_CHECK(flagsnum, flagnum)) { \
CFArrayAppendValue(flagarray, CFSTR(flagname)); } } \
while(0)
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagMustScanSubDirs, "MustScanSubDirs", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagUserDropped, "UserDropped", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagKernelDropped, "KernelDropped", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagEventIdsWrapped, "EventIdsWrapped", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagHistoryDone, "HistoryDone", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagRootChanged, "RootChanged", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagMount, "Mount", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagUnmount, "Unmount", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemCreated, "ItemCreated", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemRemoved, "ItemRemoved", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemInodeMetaMod, "ItemInodeMetaMod", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemRenamed, "ItemRenamed", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemModified, "ItemModified", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemFinderInfoMod, "ItemFinderInfoMod", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemChangeOwner, "ItemChangeOwner", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemXattrMod, "ItemXattrMod", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemIsFile, "ItemIsFile", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemIsDir, "ItemIsDir", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemIsSymlink, "ItemIsSymlink", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagOwnEvent, "OwnEvent", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemIsHardlink, "ItemIsHardLink", flags);
FLAG_ADD_NAME(eventFlags[i], kFSEventStreamEventFlagItemIsLastHardlink, "ItemIsLastHardLink", flags);
CFDictionarySetValue(event, CFSTR("flags"), flags);
CFArrayAppendValue(events, event);
CFRelease(event);
CFRelease(path);
CFRelease(ident);
CFRelease(cflags);
CFRelease(flags);
}
CFMutableDictionaryRef meta = CFDictionaryCreateMutable(kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(meta, CFSTR("events"), events);
CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberCFIndexType, &numEvents);
CFDictionarySetValue(meta, CFSTR("numEvents"), num);
CFDataRef data = TSICTStringCreateRenderedDataFromObjectWithFormat(meta, format);
fprintf(stdout, "%s", CFDataGetBytePtr(data));
CFRelease(events);
CFRelease(num);
CFRelease(meta);
CFRelease(data);
}
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;
#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, "\n");
fprintf(stderr, " event ID: %llu\n", eventIds[i]);
// STFU clang
#if defined(__LP64__)
fprintf(stderr, " event flags: %#.8x\n", eventFlags[i]);
#else
fprintf(stderr, " event flags: %#.8lx\n", eventFlags[i]);
#endif
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagMustScanSubDirs,
" Recursive scanning of directory required");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagUserDropped,
" Buffering problem: events dropped user-side");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagKernelDropped,
" Buffering problem: events dropped kernel-side");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagEventIdsWrapped,
" Event IDs have wrapped");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagHistoryDone,
" All historical events have been processed");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagRootChanged,
" Root path has changed");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagMount,
" A new volume was mounted at this path");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagUnmount,
" A volume was unmounted from this path");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemCreated,
" Item created");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemRemoved,
" Item removed");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemInodeMetaMod,
" Item metadata modified");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemRenamed,
" Item renamed");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemModified,
" Item modified");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemFinderInfoMod,
" Item Finder Info modified");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemChangeOwner,
" Item changed ownership");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemXattrMod,
" Item extended attributes modified");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemIsFile,
" Item is a file");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemIsDir,
" Item is a directory");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemIsSymlink,
" Item is a symbolic link");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemIsHardlink,
" Item is a hard link");
FLAG_CHECK_STDERR(eventFlags[i], kFSEventStreamEventFlagItemIsLastHardlink,
" Item is the last hard link");
fprintf(stderr, " event path: %s\n", paths[i]);
fprintf(stderr, "\n");
}
fprintf(stderr, "\n");
#endif
if (config.format == kFSEventWatchOutputFormatClassic) {
classic_output_format(numEvents, paths);
} else if (config.format == kFSEventWatchOutputFormatNIW) {
niw_output_format(numEvents, paths, eventFlags, eventIds);
} else if (config.format == kFSEventWatchOutputFormatTNetstring) {
tstring_output_format(numEvents, paths, eventFlags, eventIds,
kTSITStringFormatTNetstring);
} else if (config.format == kFSEventWatchOutputFormatOTNetstring) {
tstring_output_format(numEvents, paths, eventFlags, eventIds,
kTSITStringFormatOTNetstring);
}
fflush(stdout);
}
int main(int argc, const char* argv[])
{
install_signal_handlers();
parse_cli_settings(argc, argv);
if (needs_fsevents_fix) {
FSEventsFixEnable();
}
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
if (needs_fsevents_fix) {
FSEventsFixDisable();
}
FSEventStreamScheduleWithRunLoop(stream,
CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
FSEventStreamStart(stream);
CFRunLoopRun();
FSEventStreamFlushSync(stream);
FSEventStreamStop(stream);
return 0;
}