Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+13
View File
@@ -0,0 +1,13 @@
*.gem
.DS_Store
.Trashes
.bundle
.com.apple.timemachine.supported
.fseventsd
.idea
.rbx
/ext/build
Desktop DB
Desktop DF
Gemfile.lock
pkg/*
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gemspec
+8
View File
@@ -0,0 +1,8 @@
# A sample Guardfile
# More info at http://github.com/guard/guard#readme
guard :rspec do
watch(%r(^spec/(.*)_spec.rb))
watch(%r(^lib/(.*)\.rb)) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { 'spec' }
end
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2010-2014 Thibaud Guillaume-Gentil & Travis Tilley
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+260
View File
@@ -0,0 +1,260 @@
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/thibaudgg/rb-fsevent)
[![endorse](https://api.coderwall.com/ttilley/endorsecount.png)](https://coderwall.com/ttilley)
# rb-fsevent
Very simple & usable Mac OSX FSEvents API
* Signals are working (really)
* Tested on MRI 2.4.1, RBX 3.72, JRuby 1.7.26 and 9.1.8.0
* Tested on 10.8
## HFS+ filename corruption bug
There is a _very_ long-standing (since 2011) OSX bug where sometimes the filename metadata for HFS+ filesystems will get corrupted, resulting in some APIs returning one case for a file, and other APIs returning another. The result is that sometimes, _for no visible reason to the user_, fsevents would simply not work. As of rb-fsevent 0.9.5 this issue is properly detected and an insanely hacky (but effective) workaround is used that replaces the system `realpath()` with a custom implementation that should almost always return the same value as the kernel reporting (thus fixing fsevents). The major flaw in the workaround is that it may return the wrong path for hard links.
Please note that this doesn't repair the underlying issue on disk. Other apps and libraries using fsevents will continue to break with no warning. There may be other issues unrelated to fsevents.
__This bug is resolved in MacOS 10.12 and all users are strongly encouraged to upgrade.__
## Install
gem install rb-fsevent
### re-compilation
rb-fsevent comes with a pre-compiled fsevent\_watch binary supporting x86\_64 on 10.9 and above. The binary is codesigned with my (Travis Tilley) Developer ID as an extra precaution when distributing pre-compiled code and contains an embedded plist describing its build environment. This should be sufficient for most users, but if you need to use rb-fsevent on 10.8 or lower then recompilation is necessary. This can be done by entering the installed gem's ext directory and running:
MACOSX_DEPLOYMENT_TARGET="10.7" rake replace_exe
The following ENV vars are recognized:
* CC
* CFLAGS
* ARCHFLAGS
* MACOSX\_DEPLOYMENT\_TARGET
* FWDEBUG (enables debug mode, printing an obscene number of informational
messages to STDERR)
### embedded plist
You can retrieve the values in the embedded plist via the CLI:
fsevent_watch --show-plist
The output is essentially formatted as `"#{key}:\n #{value}\n"` to make it easier to read than plist style xml. The result looks like this:
DTSDKName:
macosx10.5
FSEWBuildTriple:
i386-apple-darwin10.8.0
FSEWCC:
/usr/bin/gcc-4.2
DTSDKPath:
/Developer/SDKs/MacOSX10.5.sdk
FSEWCCVersion:
i686-apple-darwin10-gcc-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5666) (dot 3)
FSEWCFLAGS:
-fconstant-cfstrings -fno-strict-aliasing -Wall -mmacosx-version-min=10.5 -O3
If, for some perverse reason, you prefer to look at the xml... it can be retrieved via:
otool -s __TEXT __info_plist ./bin/fsevent_watch | grep ^0 | xxd -r -
### codesign
You can verify code signing information for a specific fsevent\_watch via:
codesign -d -vvv ./bin/fsevent_watch
If you're using the pre-compiled binary, then the output should contain something to the effect of:
Authority=Developer ID Application: Travis Tilley
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Dec 31, 2012 12:49:13 PM
## Usage
### Singular path
```ruby
require 'rb-fsevent'
fsevent = FSEvent.new
fsevent.watch Dir.pwd do |directories|
puts "Detected change inside: #{directories.inspect}"
end
fsevent.run
```
### Multiple paths
```ruby
require 'rb-fsevent'
paths = ['/tmp/path/one', '/tmp/path/two', Dir.pwd]
fsevent = FSEvent.new
fsevent.watch paths do |directories|
puts "Detected change inside: #{directories.inspect}"
end
fsevent.run
```
### Multiple paths and additional options as a Hash
```ruby
require 'rb-fsevent'
paths = ['/tmp/path/one', '/tmp/path/two', Dir.pwd]
options = {:latency => 1.5, :no_defer => true }
fsevent = FSEvent.new
fsevent.watch paths, options do |directories|
puts "Detected change inside: #{directories.inspect}"
end
fsevent.run
```
### Multiple paths and additional options as an Array
```ruby
require 'rb-fsevent'
paths = ['/tmp/path/one', '/tmp/path/two', Dir.pwd]
options = ['--latency', 1.5, '--no-defer']
fsevent = FSEvent.new
fsevent.watch paths, options do |directories|
puts "Detected change inside: #{directories.inspect}"
end
fsevent.run
```
### Using _full_ event information
```ruby
require 'rb-fsevent'
fsevent = FSEvent.new
fsevent.watch Dir.pwd do |paths, event_meta|
event_meta['events'].each do |event|
puts "event ID: #{event['id']}"
puts "path: #{event['path']}"
puts "c flags: #{event['cflags']}"
puts "named flags: #{event['flags'].join(', ')}"
# named flags will include strings such as `ItemInodeMetaMod` or `OwnEvent`
end
end
fsevent.run
```
## Options
When defining options using a hash or hash-like object, it gets checked for validity and converted to the appropriate fsevent\_watch commandline arguments array when the FSEvent class is instantiated. This is obviously the safest and preferred method of passing in options.
You may, however, choose to pass in an array of commandline arguments as your options value and it will be passed on, unmodified, to the fsevent\_watch binary when called.
So far, the following options are supported:
* :latency => 0.5 # in seconds
* :no\_defer => true
* :watch\_root => true
* :since\_when => 18446744073709551615 # an FSEventStreamEventId
* :file\_events => true
### Latency
The :latency parameter determines how long the service should wait after the first event before passing that information along to the client. If your latency is set to 4 seconds, and 300 changes occur in the first three, then the callback will be fired only once. If latency is set to 0.1 in the exact same scenario, you will see that callback fire somewhere closer to between 25 and 30 times.
Setting a higher latency value allows for more effective temporal coalescing, resulting in fewer callbacks and greater overall efficiency... at the cost of apparent responsiveness. Setting this to a reasonably high value (and NOT setting :no\_defer) is particularly well suited for background, daemon, or batch processing applications.
Implementation note: It appears that FSEvents will only coalesce events from a maximum of 32 distinct subpaths, making the above completely accurate only when events are to fewer than 32 subpaths. Creating 300 files in one directory, for example, or 30 files in 10 subdirectories, but not 300 files within 300 subdirectories. In the latter case, you may receive 31 callbacks in one go after the latency period. As this appears to be an implementation detail, the number could potentially differ across OS revisions. It is entirely possible that this number is somehow configurable, but I have not yet discovered an accepted method of doing so.
### NoDefer
The :no\_defer option changes the behavior of the latency parameter completely. Rather than waiting for $latency period of time before sending along events in an attempt to coalesce a potential deluge ahead of time, that first event is sent along to the client immediately and is followed by a $latency period of silence before sending along any additional events that occurred within that period.
This behavior is particularly useful for interactive applications where that feeling of apparent responsiveness is most important, but you still don't want to get overwhelmed by a series of events that occur in rapid succession.
### WatchRoot
The :watch\_root option allows for catching the scenario where you start watching "~/src/demo\_project" and either it is later renamed to "~/src/awesome\_sauce\_3000" or the path changes in such a manner that the original directory is now at "~/clients/foo/iteration4/demo\_project".
Unfortunately, while this behavior is somewhat supported in the fsevent\_watch binary built as part of this project, support for passing across detailed metadata is not (yet). As a result, you would not receive the appropriate RootChanged event and be able to react appropriately. Also, since the C code doesn't open watched directories and retain that file descriptor as part of path-specific callback metadata, we are unable to issue an F\_GETPATH fcntl() to determine the directory's new path.
Please do not use this option until proper support is added (or, even better, add it and submit a pull request).
### SinceWhen
The FSEventStreamEventId passed in to :since\_when is used as a base for reacting to historic events. Unfortunately, not only is the metadata for transitioning from historic to live events not currently passed along, but it is incorrectly passed as a change event on the root path, and only per-host event streams are currently supported. When using per-host event streams, the event IDs are not guaranteed to be unique or contiguous when shared volumes (firewire/USB/net/etc) are used on multiple macs.
Please do not use this option until proper support is added, unless it's acceptable for you to receive that one fake event that's handled incorrectly when events transition from historical to live. Even in that scenario, there's no metadata available for determining the FSEventStreamEventId of the last received event.
WARNING: passing in 0 as the parameter to :since\_when will return events for every directory modified since "the beginning of time".
### FileEvents ###
Prepare yourself for an obscene number of callbacks. Realistically, an "Atomic Save" could easily fire maybe 6 events for the combination of creating the new file, changing metadata/permissions, writing content, swapping out the old file for the new may itself result in multiple events being fired, and so forth. By the time you get the event for the temporary file being created as part of the atomic save, it will already be gone and swapped with the original file. This and issues of a similar nature have prevented me from adding the option to the ruby code despite the fsevent\_watch binary supporting file level events for quite some time now. Mountain Lion seems to be better at coalescing needless events, but that might just be my imagination.
## Debugging output
If the gem is re-compiled with the environment variable FWDEBUG set, then fsevent\_watch will be built with its various DEBUG sections defined, and the output to STDERR is truly verbose (and hopefully helpful in debugging your application and not just fsevent\_watch itself). If enough people find this to be directly useful when developing code that makes use of rb-fsevent, then it wouldn't be hard to clean this up and make it a feature enabled by a commandline argument instead. Until somebody files an issue, however, I will assume otherwise.
append_path called for: /tmp/moo/cow/
resolved path to: /private/tmp/moo/cow
config.sinceWhen 18446744073709551615
config.latency 0.300000
config.flags 00000000
config.paths
/private/tmp/moo/cow
FSEventStreamRef @ 0x100108540:
allocator = 0x7fff705a4ee0
callback = 0x10000151e
context = {0, 0x0, 0x0, 0x0, 0x0}
numPathsToWatch = 1
pathsToWatch = 0x7fff705a4ee0
pathsToWatch[0] = '/private/tmp/moo/cow'
latestEventId = -1
latency = 300000 (microseconds)
flags = 0x00000000
runLoop = 0x0
runLoopMode = 0x0
FSEventStreamCallback fired!
numEvents: 32
event path: /private/tmp/moo/cow/1/a/
event flags: 00000000
event ID: 1023767
event path: /private/tmp/moo/cow/1/b/
event flags: 00000000
event ID: 1023782
event path: /private/tmp/moo/cow/1/c/
event flags: 00000000
event ID: 1023797
event path: /private/tmp/moo/cow/1/d/
event flags: 00000000
event ID: 1023812
[etc]
## Development
* Source hosted at [GitHub](http://github.com/thibaudgg/rb-fsevent)
* Report issues/Questions/Feature requests on [GitHub Issues](http://github.com/thibaudgg/rb-fsevent/issues)
Pull requests are quite welcome! Please ensure that your commits are in a topic branch for each individual changeset that can be reasonably isolated. It is also important to ensure that your changes are well tested... whether that means new tests, modified tests, or fixing a scenario where the existing tests currently fail. If you have rbenv and ruby-build, we have a helper task for running the testsuite in all of them:
rake spec:portability
The list of tested targets is currently:
%w[2.4.1 rbx-3.72 jruby-1.7.26 jruby-9.1.8.0]
## Authors
* [Travis Tilley](http://github.com/ttilley)
* [Thibaud Guillaume-Gentil](http://github.com/thibaudgg)
* [Andrey Tarantsov](https://github.com/andreyvit)
+33
View File
@@ -0,0 +1,33 @@
# -*- encoding: utf-8 -*-
require 'bundler'
Bundler::GemHelper.install_tasks
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
namespace(:spec) do
desc "Run all specs on multiple ruby versions"
task(:portability) do
versions = %w[2.4.1 rbx-3.72 jruby-1.7.26 jruby-9.1.8.0]
versions.each do |version|
# system <<-BASH
# bash -c 'source ~/.rvm/scripts/rvm;
# rvm #{version};
# echo "--------- version #{version} ----------\n";
# bundle install;
# rake spec'
# BASH
system <<-BASH
bash -c 'export PATH="$HOME/.rbenv/bin:$PATH";
[[ `which rbenv` ]] && eval "$(rbenv init -)";
[[ ! -a $HOME/.rbenv/versions/#{version} ]] && rbenv install #{version};
rbenv shell #{version};
rbenv which bundle 2> /dev/null || gem install bundler;
rm Gemfile.lock;
bundle install;
rake spec;'
BASH
end
end
end
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2011-2013 Travis Tilley
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,626 @@
/*
* FSEventsFix
*
* Resolves a long-standing bug in realpath() that prevents FSEvents API from
* monitoring certain folders on a wide range of OS X released (10.6-10.10 at least).
*
* The underlying issue is that for some folders, realpath() call starts returning
* a path with incorrect casing (e.g. "/users/smt" instead of "/Users/smt").
* FSEvents is case-sensitive and calls realpath() on the paths you pass in, so
* an incorrect value returned by realpath() prevents FSEvents from seeing any
* change events.
*
* See the discussion at https://github.com/thibaudgg/rb-fsevent/issues/10 about
* the history of this bug and how this library came to exist.
*
* This library uses Facebook's fishhook to replace a custom implementation of
* realpath in place of the system realpath; FSEvents will then invoke our custom
* implementation (which does not screw up the names) and will thus work correctly.
*
* Our implementation of realpath is based on the open-source implementation from
* OS X 10.10, with a single change applied (enclosed in "BEGIN WORKAROUND FOR
* OS X BUG" ... "END WORKAROUND FOR OS X BUG").
*
* Include FSEventsFix.{h,c} into your project and call FSEventsFixInstall().
*
* It is recommended that you install FSEventsFix on demand, using FSEventsFixIsBroken
* to check if the folder you're about to pass to FSEventStreamCreate needs the fix.
* Note that the fix must be applied before calling FSEventStreamCreate.
*
* FSEventsFixIsBroken requires a path that uses the correct case for all folder names,
* i.e. a path provided by the system APIs or constructed from folder names provided
* by the directory enumeration APIs.
*
* Copyright (c) 2015 Andrey Tarantsov <andrey@tarantsov.com>
* Copyright (c) 2003 Constantin S. Svintsoff <kostik@iclub.nsu.ru>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Based on a realpath implementation from Apple libc 498.1.7, taken from
* http://www.opensource.apple.com/source/Libc/Libc-498.1.7/stdlib/FreeBSD/realpath.c
* and provided under the following license:
*
* Copyright (c) 2003 Constantin S. Svintsoff <kostik@iclub.nsu.ru>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include "FSEventsFix.h"
#include <dispatch/dispatch.h>
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <dlfcn.h>
const char *const FSEventsFixVersionString = "0.11.0";
#pragma mark - Forward declarations
static char *(*orig_realpath)(const char *restrict file_name, char resolved_name[PATH_MAX]);
static char *CFURL_realpath(const char *restrict file_name, char resolved_name[PATH_MAX]);
static char *FSEventsFix_realpath_wrapper(const char *restrict src, char *restrict dst);
static void _FSEventsFixHookInstall();
static void _FSEventsFixHookUninstall();
#pragma mark - Internal state
static dispatch_queue_t g_queue = NULL;
static int64_t g_enable_refcount = 0;
static bool g_in_self_test = false;
static bool g_hook_operational = false;
static void(^g_logging_block)(FSEventsFixMessageType type, const char *message);
static FSEventsFixDebugOptions g_debug_opt = 0;
typedef struct {
char *name;
void *replacement;
void *original;
uint hooked_symbols;
} rebinding_t;
static rebinding_t g_rebindings[] = {
{ "_realpath$DARWIN_EXTSN", (void *) &FSEventsFix_realpath_wrapper, (void *) &realpath, 0 }
};
static const uint g_rebindings_nel = sizeof(g_rebindings) / sizeof(g_rebindings[0]);
#pragma mark - Logging
static void _FSEventsFixLog(FSEventsFixMessageType type, const char *__restrict fmt, ...) __attribute__((__format__ (__printf__, 2, 3)));
static void _FSEventsFixLog(FSEventsFixMessageType type, const char *__restrict fmt, ...) {
if (g_logging_block) {
char *message = NULL;
va_list va;
va_start(va, fmt);
vasprintf(&message, fmt, va);
va_end(va);
if (message) {
if (!!(g_debug_opt & FSEventsFixDebugOptionLogToStderr)) {
fprintf(stderr, "FSEventsFix: %s\n", message);
}
if (g_logging_block) {
g_logging_block(type, message);
}
free(message);
}
}
}
#pragma mark - API
void _FSEventsFixInitialize() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
g_queue = dispatch_queue_create("FSEventsFix", DISPATCH_QUEUE_SERIAL);
});
}
void FSEventsFixConfigure(FSEventsFixDebugOptions debugOptions, void(^loggingBlock)(FSEventsFixMessageType severity, const char *message)) {
_FSEventsFixInitialize();
loggingBlock = Block_copy(loggingBlock);
dispatch_sync(g_queue, ^{
g_debug_opt = debugOptions;
g_logging_block = loggingBlock;
});
}
// Must be called from the private serial queue.
void _FSEventsFixSelfTest() {
g_in_self_test = true;
g_hook_operational = false;
static char result[1024];
realpath("/Etc/__!FSEventsFixSelfTest!__", result);
g_in_self_test = false;
}
void FSEventsFixEnable() {
_FSEventsFixInitialize();
dispatch_sync(g_queue, ^{
if (++g_enable_refcount == 1) {
orig_realpath = dlsym(RTLD_DEFAULT, "realpath");
_FSEventsFixHookInstall();
_FSEventsFixSelfTest();
if (g_hook_operational) {
_FSEventsFixLog(FSEventsFixMessageTypeStatusChange, "Enabled");
} else {
_FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Failed to enable (hook not called)");
}
}
});
}
void FSEventsFixDisable() {
_FSEventsFixInitialize();
dispatch_sync(g_queue, ^{
if (g_enable_refcount == 0) {
abort();
}
if (--g_enable_refcount == 0) {
_FSEventsFixHookUninstall();
_FSEventsFixSelfTest();
if (!g_hook_operational) {
_FSEventsFixLog(FSEventsFixMessageTypeStatusChange, "Disabled");
} else {
_FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Failed to disable (hook still called)");
}
}
});
}
bool FSEventsFixIsOperational() {
_FSEventsFixInitialize();
__block bool result = false;
dispatch_sync(g_queue, ^{
result = g_hook_operational;
});
return result;
}
bool _FSEventsFixIsBroken_noresolve(const char *resolved) {
if (!!(g_debug_opt & FSEventsFixDebugOptionSimulateBroken)) {
if (strstr(resolved, FSEventsFixSimulatedBrokenFolderMarker)) {
return true;
}
}
char *reresolved = realpath(resolved, NULL);
if (reresolved) {
bool broken = (0 != strcmp(resolved, reresolved));
free(reresolved);
return broken;
} else {
return true;
}
}
bool FSEventsFixIsBroken(const char *path) {
char *resolved = CFURL_realpath(path, NULL);
if (!resolved) {
return true;
}
bool broken = _FSEventsFixIsBroken_noresolve(resolved);
free(resolved);
return broken;
}
char *FSEventsFixCopyRootBrokenFolderPath(const char *inpath) {
if (!FSEventsFixIsBroken(inpath)) {
return NULL;
}
// get a mutable copy of an absolute path
char *path = CFURL_realpath(inpath, NULL);
if (!path) {
return NULL;
}
for (;;) {
char *sep = strrchr(path, '/');
if ((sep == NULL) || (sep == path)) {
break;
}
*sep = 0;
if (!_FSEventsFixIsBroken_noresolve(path)) {
*sep = '/';
break;
}
}
return path;
}
static void _FSEventsFixAttemptRepair(const char *folder) {
int rv = rename(folder, folder);
if (!!(g_debug_opt & FSEventsFixDebugOptionSimulateRepair)) {
const char *pos = strstr(folder, FSEventsFixSimulatedBrokenFolderMarker);
if (pos) {
char *fixed = strdup(folder);
fixed[pos - folder] = 0;
strcat(fixed, pos + strlen(FSEventsFixSimulatedBrokenFolderMarker));
rv = rename(folder, fixed);
free(fixed);
}
}
if (rv != 0) {
if (errno == EPERM) {
_FSEventsFixLog(FSEventsFixMessageTypeResult, "Permission error when trying to repair '%s'", folder);
} else {
_FSEventsFixLog(FSEventsFixMessageTypeExpectedFailure, "Unknown error when trying to repair '%s': errno = %d", folder, errno);
}
}
}
FSEventsFixRepairStatus FSEventsFixRepairIfNeeded(const char *inpath) {
char *root = FSEventsFixCopyRootBrokenFolderPath(inpath);
if (root == NULL) {
return FSEventsFixRepairStatusNotBroken;
}
for (;;) {
_FSEventsFixAttemptRepair(root);
char *newRoot = FSEventsFixCopyRootBrokenFolderPath(inpath);
if (newRoot == NULL) {
_FSEventsFixLog(FSEventsFixMessageTypeResult, "Repaired '%s' in '%s'", root, inpath);
free(root);
return FSEventsFixRepairStatusRepaired;
}
if (0 == strcmp(root, newRoot)) {
_FSEventsFixLog(FSEventsFixMessageTypeResult, "Failed to repair '%s' in '%s'", root, inpath);
free(root);
free(newRoot);
return FSEventsFixRepairStatusFailed;
}
_FSEventsFixLog(FSEventsFixMessageTypeResult, "Partial success, repaired '%s' in '%s'", root, inpath);
free(root);
root = newRoot;
}
}
#pragma mark - FSEventsFix realpath wrapper
static char *FSEventsFix_realpath_wrapper(const char * __restrict src, char * __restrict dst) {
if (g_in_self_test) {
if (strstr(src, "__!FSEventsFixSelfTest!__")) {
g_hook_operational = true;
}
}
// CFURL_realpath doesn't support putting where resolution failed into the
// dst buffer, so we call the original realpath here first and if it gets a
// result, replace that with the output of CFURL_realpath. that way all the
// features of the original realpath are available.
char *rv = NULL;
char *orv = orig_realpath(src, dst);
if (orv != NULL) { rv = CFURL_realpath(src, dst); }
if (!!(g_debug_opt & FSEventsFixDebugOptionLogCalls)) {
char *result = rv ?: dst;
_FSEventsFixLog(FSEventsFixMessageTypeCall, "realpath(%s) => %s\n", src, result);
}
if (!!(g_debug_opt & FSEventsFixDebugOptionUppercaseReturn)) {
char *result = rv ?: dst;
if (result) {
for (char *pch = result; *pch; ++pch) {
*pch = (char)toupper(*pch);
}
}
}
return rv;
}
#pragma mark - realpath
// naive implementation of realpath on top of CFURL
// NOTE: doesn't quite support the full range of errno results one would
// expect here, in part because some of these functions just return a boolean,
// and in part because i'm not dealing with messy CFErrorRef objects and
// attempting to translate those to sane errno values.
// NOTE: the OSX realpath will return _where_ resolution failed in resolved_name
// if passed in and return NULL. we can't properly support that extension here
// since the resolution happens entirely behind the scenes to us in CFURL.
static char* CFURL_realpath(const char *file_name, char resolved_name[PATH_MAX])
{
char* resolved;
CFURLRef url1;
CFURLRef url2;
CFStringRef path;
if (file_name == NULL) {
errno = EINVAL;
return (NULL);
}
#if __DARWIN_UNIX03
if (*file_name == 0) {
errno = ENOENT;
return (NULL);
}
#endif
// create a buffer to store our result if we weren't passed one
if (!resolved_name) {
if ((resolved = malloc(PATH_MAX)) == NULL) return (NULL);
} else {
resolved = resolved_name;
}
url1 = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8*)file_name, (CFIndex)strlen(file_name), false);
if (url1 == NULL) { goto error_return; }
url2 = CFURLCopyAbsoluteURL(url1);
CFRelease(url1);
if (url2 == NULL) { goto error_return; }
url1 = CFURLCreateFileReferenceURL(NULL, url2, NULL);
CFRelease(url2);
if (url1 == NULL) { goto error_return; }
// if there are multiple hard links to the original path, this may end up
// being _completely_ different from what was intended
url2 = CFURLCreateFilePathURL(NULL, url1, NULL);
CFRelease(url1);
if (url2 == NULL) { goto error_return; }
path = CFURLCopyFileSystemPath(url2, kCFURLPOSIXPathStyle);
CFRelease(url2);
if (path == NULL) { goto error_return; }
bool success = CFStringGetCString(path, resolved, PATH_MAX, kCFStringEncodingUTF8);
CFRelease(path);
if (!success) { goto error_return; }
return resolved;
error_return:
if (!resolved_name) {
// we weren't passed in an output buffer and created our own. free it
int e = errno;
free(resolved);
errno = e;
}
return (NULL);
}
#pragma mark - fishhook
// Copyright (c) 2013, Facebook, Inc.
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name Facebook nor the names of its contributors may be used to
// endorse or promote products derived from this software without specific
// prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#import <stdlib.h>
#import <string.h>
#import <sys/types.h>
#import <mach-o/dyld.h>
#import <mach-o/loader.h>
#import <mach-o/nlist.h>
#ifdef __LP64__
typedef struct mach_header_64 mach_header_t;
typedef struct segment_command_64 segment_command_t;
typedef struct section_64 section_t;
typedef struct nlist_64 nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
#else
typedef struct mach_header mach_header_t;
typedef struct segment_command segment_command_t;
typedef struct section section_t;
typedef struct nlist nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT
#endif
static volatile bool g_hook_installed = false;
static void _FSEventsFixHookUpdateSection(section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab)
{
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
for (uint i = 0; i < section->size / sizeof(void *); i++) {
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
char *symbol_name = strtab + strtab_offset;
for (rebinding_t *cur = g_rebindings, *end = g_rebindings + g_rebindings_nel; cur < end; ++cur) {
if (strcmp(symbol_name, cur->name) == 0) {
if (g_hook_installed) {
if (indirect_symbol_bindings[i] != cur->replacement) {
indirect_symbol_bindings[i] = cur->replacement;
++cur->hooked_symbols;
}
} else if (cur->original != NULL) {
if (indirect_symbol_bindings[i] == cur->replacement) {
indirect_symbol_bindings[i] = cur->original;
if (cur->hooked_symbols > 0) {
--cur->hooked_symbols;
}
}
}
goto symbol_loop;
}
}
symbol_loop:;
}
}
static void _FSEventsFixHookUpdateImage(const struct mach_header *header, intptr_t slide) {
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
_FSEventsFixHookUpdateSection(sect, slide, symtab, strtab, indirect_symtab);
}
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
_FSEventsFixHookUpdateSection(sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
static void _FSEventsFixHookSaveOriginals() {
for (rebinding_t *cur = g_rebindings, *end = g_rebindings + g_rebindings_nel; cur < end; ++cur) {
void *original = cur->original = dlsym(RTLD_DEFAULT, cur->name+1);
if (!original) {
const char *error = dlerror();
_FSEventsFixLog(FSEventsFixMessageTypeFatalError, "Cannot find symbol %s, dlsym says: %s\n", cur->name, error);
}
}
}
static void _FSEventsFixHookUpdate() {
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_FSEventsFixHookUpdateImage(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
static void _FSEventsFixHookInstall() {
static bool first_rebinding_done = false;
if (!g_hook_installed) {
g_hook_installed = true;
if (!first_rebinding_done) {
first_rebinding_done = true;
_FSEventsFixHookSaveOriginals();
_dyld_register_func_for_add_image(_FSEventsFixHookUpdateImage);
} else {
_FSEventsFixHookUpdate();
}
}
}
static void _FSEventsFixHookUninstall() {
if (g_hook_installed) {
g_hook_installed = false;
_FSEventsFixHookUpdate();
}
}
@@ -0,0 +1,105 @@
/*
* FSEventsFix
*
* Works around a long-standing bug in realpath() that prevents FSEvents API from
* monitoring certain folders on a wide range of OS X releases (10.6-10.10 at least).
*
* The underlying issue is that for some folders, realpath() call starts returning
* a path with incorrect casing (e.g. "/users/smt" instead of "/Users/smt").
* FSEvents is case-sensitive and calls realpath() on the paths you pass in, so
* an incorrect value returned by realpath() prevents FSEvents from seeing any
* change events.
*
* See the discussion at https://github.com/thibaudgg/rb-fsevent/issues/10 about
* the history of this bug and how this library came to exist.
*
* This library uses Facebook's fishhook to replace a custom implementation of
* realpath in place of the system realpath; FSEvents will then invoke our custom
* implementation (which does not screw up the names) and will thus work correctly.
*
* Our implementation of realpath is based on the open-source implementation from
* OS X 10.10, with a single change applied (enclosed in "BEGIN WORKAROUND FOR
* OS X BUG" ... "END WORKAROUND FOR OS X BUG").
*
* Include FSEventsFix.{h,c} into your project and call FSEventsFixInstall().
*
* It is recommended that you install FSEventsFix on demand, using FSEventsFixIsBroken
* to check if the folder you're about to pass to FSEventStreamCreate needs the fix.
* Note that the fix must be applied before calling FSEventStreamCreate.
*
* FSEventsFixIsBroken requires a path that uses the correct case for all folder names,
* i.e. a path provided by the system APIs or constructed from folder names provided
* by the directory enumeration APIs.
*
* See .c file for license & copyrights, but basically this is available under a mix
* of MIT and BSD licenses.
*/
#ifndef __FSEventsFix__
#define __FSEventsFix__
#include <CoreFoundation/CoreFoundation.h>
/// A library version string (e.g. 1.2.3) for displaying and logging purposes
extern const char *const FSEventsFixVersionString;
/// See FSEventsFixDebugOptionSimulateBroken
#define FSEventsFixSimulatedBrokenFolderMarker "__!FSEventsBroken!__"
typedef CF_OPTIONS(unsigned, FSEventsFixDebugOptions) {
/// Always return an uppercase string from realpath
FSEventsFixDebugOptionUppercaseReturn = 0x01,
/// Log all calls to realpath using the logger configured via FSEventsFixConfigure
FSEventsFixDebugOptionLogCalls = 0x02,
/// In addition to the logging block (if any), log everything to stderr
FSEventsFixDebugOptionLogToStderr = 0x08,
/// Report paths containing FSEventsFixSimulatedBrokenFolderMarker as broken
FSEventsFixDebugOptionSimulateBroken = 0x10,
/// Repair paths containing FSEventsFixSimulatedBrokenFolderMarker by renaming them
FSEventsFixDebugOptionSimulateRepair = 0x20,
};
typedef CF_ENUM(int, FSEventsFixMessageType) {
/// Call logging requested via FSEventsFixDebugOptionLogCalls
FSEventsFixMessageTypeCall,
/// Results of actions like repair, and other pretty verbose, but notable, stuff.
FSEventsFixMessageTypeResult,
/// Enabled/disabled status change
FSEventsFixMessageTypeStatusChange,
/// Expected failure (treat as a warning)
FSEventsFixMessageTypeExpectedFailure,
/// Severe failure that most likely means that the library won't work
FSEventsFixMessageTypeFatalError
};
typedef CF_ENUM(int, FSEventsFixRepairStatus) {
FSEventsFixRepairStatusNotBroken,
FSEventsFixRepairStatusRepaired,
FSEventsFixRepairStatusFailed,
};
/// Note that the logging block can be called on any dispatch queue.
void FSEventsFixConfigure(FSEventsFixDebugOptions debugOptions, void(^loggingBlock)(FSEventsFixMessageType type, const char *message));
void FSEventsFixEnable();
void FSEventsFixDisable();
bool FSEventsFixIsOperational();
bool FSEventsFixIsBroken(const char *path);
/// If the path is broken, returns a string identifying the root broken folder,
/// otherwise, returns NULL. You need to free() the returned string.
char *FSEventsFixCopyRootBrokenFolderPath(const char *path);
FSEventsFixRepairStatus FSEventsFixRepairIfNeeded(const char *path);
#endif
@@ -0,0 +1,373 @@
//
// TSICTString.c
// TSITString
//
// Created by Travis Tilley on 9/27/11.
//
#include "TSICTString.h"
const char* const TNetstringTypes = ",#^!~}]Z";
const char* const OTNetstringTypes = ",#^!~{[Z";
const UInt8 TNetstringSeparator = ':';
TSITStringFormat TSITStringDefaultFormat = kTSITStringFormatTNetstring;
static const CFRange BeginningRange = {0,0};
static CFTypeID kCFDataTypeID = -1UL;
static CFTypeID kCFStringTypeID = -1UL;
static CFTypeID kCFNumberTypeID = -1UL;
static CFTypeID kCFBooleanTypeID = -1UL;
static CFTypeID kCFNullTypeID = -1UL;
static CFTypeID kCFArrayTypeID = -1UL;
static CFTypeID kCFDictionaryTypeID = -1UL;
__attribute__((constructor)) void Init_TSICTString(void)
{
kCFDataTypeID = CFDataGetTypeID();
kCFStringTypeID = CFStringGetTypeID();
kCFNumberTypeID = CFNumberGetTypeID();
kCFBooleanTypeID = CFBooleanGetTypeID();
kCFNullTypeID = CFNullGetTypeID();
kCFArrayTypeID = CFArrayGetTypeID();
kCFDictionaryTypeID = CFDictionaryGetTypeID();
}
void TSICTStringSetDefaultFormat(TSITStringFormat format)
{
if (format == kTSITStringFormatDefault) {
TSITStringDefaultFormat = kTSITStringFormatTNetstring;
} else {
TSITStringDefaultFormat = format;
}
}
TSITStringFormat TSICTStringGetDefaultFormat(void)
{
return TSITStringDefaultFormat;
}
void TSICTStringDestroy(TStringIRep* rep)
{
CFRelease(rep->data);
free(rep->length);
free(rep);
}
static inline TStringIRep* TSICTStringCreateWithDataOfTypeAndFormat(CFDataRef data, TSITStringTag type, TSITStringFormat format)
{
if (format == kTSITStringFormatDefault) {
format = TSICTStringGetDefaultFormat();
}
TStringIRep* rep = calloc(1, sizeof(TStringIRep));
rep->data = CFDataCreateCopy(kCFAllocatorDefault, data);
rep->type = type;
rep->format = format;
rep->length = calloc(10, sizeof(char));
CFIndex len = CFDataGetLength(rep->data);
if (snprintf(rep->length, 10, "%lu", len)) {
return rep;
} else {
TSICTStringDestroy(rep);
return NULL;
}
}
static inline CFDataRef TSICTStringCreateDataFromIntermediateRepresentation(TStringIRep* rep)
{
CFIndex len = CFDataGetLength(rep->data);
CFMutableDataRef buffer = CFDataCreateMutableCopy(kCFAllocatorDefault, (len + 12), rep->data);
UInt8* bufferBytes = CFDataGetMutableBytePtr(buffer);
size_t prefixLength = strlen(rep->length) + 1;
CFDataReplaceBytes(buffer, BeginningRange, (const UInt8*)rep->length, (CFIndex)prefixLength);
if (rep->format == kTSITStringFormatTNetstring) {
const UInt8 ftag = (UInt8)TNetstringTypes[rep->type];
CFDataAppendBytes(buffer, &ftag, 1);
bufferBytes[(prefixLength - 1)] = TNetstringSeparator;
} else if (rep->format == kTSITStringFormatOTNetstring) {
const UInt8 ftag = (UInt8)OTNetstringTypes[rep->type];
bufferBytes[(prefixLength - 1)] = ftag;
}
CFDataRef dataRep = CFDataCreateCopy(kCFAllocatorDefault, buffer);
CFRelease(buffer);
return dataRep;
}
static inline CFStringRef TSICTStringCreateStringFromIntermediateRepresentation(TStringIRep* rep)
{
CFDataRef data = TSICTStringCreateDataFromIntermediateRepresentation(rep);
CFStringRef string = CFStringCreateFromExternalRepresentation(kCFAllocatorDefault, data, kCFStringEncodingUTF8);
CFRelease(data);
return string;
}
static inline void TSICTStringAppendObjectToMutableDataWithFormat(CFTypeRef object, CFMutableDataRef buffer, TSITStringFormat format)
{
if (object == NULL) {
object = kCFNull;
}
CFRetain(object);
TStringIRep* objRep = TSICTStringCreateWithObjectAndFormat(object, format);
CFDataRef objData = TSICTStringCreateDataFromIntermediateRepresentation(objRep);
CFDataAppendBytes(buffer, (CFDataGetBytePtr(objData)), CFDataGetLength(objData));
CFRelease(objData);
TSICTStringDestroy(objRep);
CFRelease(object);
}
static void ArrayBufferAppendCallback(const void* item, void* context)
{
TStringCollectionCallbackContext* cx = (TStringCollectionCallbackContext*)context;
CFMutableDataRef buffer = cx->buffer;
TSITStringFormat format = cx->format;
TSICTStringAppendObjectToMutableDataWithFormat(item, buffer, format);
}
static void DictionaryBufferAppendCallback(const void* key, const void* value, void* context)
{
TStringCollectionCallbackContext* cx = (TStringCollectionCallbackContext*)context;
CFMutableDataRef buffer = cx->buffer;
TSITStringFormat format = cx->format;
TSICTStringAppendObjectToMutableDataWithFormat(key, buffer, format);
TSICTStringAppendObjectToMutableDataWithFormat(value, buffer, format);
}
CFDataRef TSICTStringCreateRenderedData(TStringIRep* rep)
{
return TSICTStringCreateDataFromIntermediateRepresentation(rep);
}
CFDataRef TSICTStringCreateRenderedDataFromObjectWithFormat(CFTypeRef object, TSITStringFormat format)
{
if (object == NULL) {
object = kCFNull;
}
CFRetain(object);
TStringIRep* rep = TSICTStringCreateWithObjectAndFormat(object, format);
CFDataRef data = TSICTStringCreateDataFromIntermediateRepresentation(rep);
TSICTStringDestroy(rep);
CFRelease(object);
return data;
}
CFStringRef TSICTStringCreateRenderedString(TStringIRep* rep)
{
return TSICTStringCreateStringFromIntermediateRepresentation(rep);
}
CFStringRef TSICTStringCreateRenderedStringFromObjectWithFormat(CFTypeRef object, TSITStringFormat format)
{
if (object == NULL) {
object = kCFNull;
}
CFRetain(object);
TStringIRep* rep = TSICTStringCreateWithObjectAndFormat(object, format);
CFStringRef string = TSICTStringCreateStringFromIntermediateRepresentation(rep);
TSICTStringDestroy(rep);
CFRelease(object);
return string;
}
TStringIRep* TSICTStringCreateWithObjectAndFormat(CFTypeRef object, TSITStringFormat format)
{
if (object == NULL) {
return TSICTStringCreateNullWithFormat(format);
}
CFRetain(object);
CFTypeID cfType = CFGetTypeID(object);
TStringIRep* rep = NULL;
if (cfType == kCFDataTypeID) {
rep = TSICTStringCreateWithDataOfTypeAndFormat(object, kTSITStringTagString, format);
} else if (cfType == kCFStringTypeID) {
rep = TSICTStringCreateWithStringAndFormat(object, format);
} else if (cfType == kCFNumberTypeID) {
rep = TSICTStringCreateWithNumberAndFormat(object, format);
} else if (cfType == kCFBooleanTypeID) {
if (CFBooleanGetValue(object)) {
rep = TSICTStringCreateTrueWithFormat(format);
} else {
rep = TSICTStringCreateFalseWithFormat(format);
}
} else if (cfType == kCFNullTypeID) {
rep = TSICTStringCreateNullWithFormat(format);
} else if (cfType == kCFArrayTypeID) {
rep = TSICTStringCreateWithArrayAndFormat(object, format);
} else if (cfType == kCFDictionaryTypeID) {
rep = TSICTStringCreateWithDictionaryAndFormat(object, format);
} else {
rep = TSICTStringCreateInvalidWithFormat(format);
}
CFRelease(object);
return rep;
}
TStringIRep* TSICTStringCreateWithStringAndFormat(CFStringRef string, TSITStringFormat format)
{
CFRetain(string);
CFDataRef data = CFStringCreateExternalRepresentation(kCFAllocatorDefault, string, kCFStringEncodingUTF8, '?');
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, kTSITStringTagString, format);
CFRelease(data);
CFRelease(string);
return rep;
}
TStringIRep* TSICTStringCreateWithNumberAndFormat(CFNumberRef number, TSITStringFormat format)
{
CFRetain(number);
TSITStringTag tag = kTSITStringTagNumber;
CFDataRef data;
CFNumberType numType = CFNumberGetType(number);
switch(numType) {
case kCFNumberCharType:
{
int value;
if (CFNumberGetValue(number, kCFNumberIntType, &value)) {
if (value == 0 || value == 1) {
tag = kTSITStringTagBool;
} else {
tag = kTSITStringTagString;
}
}
break;
}
case kCFNumberFloat32Type:
case kCFNumberFloat64Type:
case kCFNumberFloatType:
case kCFNumberDoubleType:
{
tag = kTSITStringTagFloat;
break;
}
}
if (tag == kTSITStringTagBool) {
bool value;
CFNumberGetValue(number, kCFNumberIntType, &value);
if (value) {
data = CFDataCreate(kCFAllocatorDefault, (UInt8*)"true", 4);
} else {
data = CFDataCreate(kCFAllocatorDefault, (UInt8*)"false", 5);
}
} else if (tag == kTSITStringTagFloat) {
char buf[32];
char *p, *e;
double value;
CFNumberGetValue(number, numType, &value);
sprintf(buf, "%#.15g", value);
e = buf + strlen(buf);
p = e;
while (p[-1]=='0' && ('0' <= p[-2] && p[-2] <= '9')) {
p--;
}
memmove(p, e, strlen(e)+1);
data = CFDataCreate(kCFAllocatorDefault, (UInt8*)buf, (CFIndex)strlen(buf));
} else {
char buf[32];
SInt64 value;
CFNumberGetValue(number, numType, &value);
sprintf(buf, "%lli", value);
data = CFDataCreate(kCFAllocatorDefault, (UInt8*)buf, (CFIndex)strlen(buf));
}
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, tag, format);
CFRelease(data);
CFRelease(number);
return rep;
}
TStringIRep* TSICTStringCreateTrueWithFormat(TSITStringFormat format)
{
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (UInt8*)"true", 4);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, kTSITStringTagBool, format);
CFRelease(data);
return rep;
}
TStringIRep* TSICTStringCreateFalseWithFormat(TSITStringFormat format)
{
CFDataRef data = CFDataCreate(kCFAllocatorDefault, (UInt8*)"false", 5);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, kTSITStringTagBool, format);
CFRelease(data);
return rep;
}
TStringIRep* TSICTStringCreateNullWithFormat(TSITStringFormat format)
{
CFDataRef data = CFDataCreate(kCFAllocatorDefault, NULL, 0);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, kTSITStringTagNull, format);
CFRelease(data);
return rep;
}
TStringIRep* TSICTStringCreateInvalidWithFormat(TSITStringFormat format)
{
CFDataRef data = CFDataCreate(kCFAllocatorDefault, NULL, 0);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(data, kTSITStringTagInvalid, format);
CFRelease(data);
return rep;
}
TStringIRep* TSICTStringCreateWithArrayAndFormat(CFArrayRef array, TSITStringFormat format)
{
CFRetain(array);
CFMutableDataRef buffer = CFDataCreateMutable(kCFAllocatorDefault, 0);
CFRange all = CFRangeMake(0, CFArrayGetCount(array));
TStringCollectionCallbackContext cx = {buffer, format};
CFArrayApplyFunction(array, all, ArrayBufferAppendCallback, &cx);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(buffer, kTSITStringTagList, format);
CFRelease(buffer);
CFRelease(array);
return rep;
}
TStringIRep* TSICTStringCreateWithDictionaryAndFormat(CFDictionaryRef dictionary, TSITStringFormat format)
{
CFRetain(dictionary);
CFMutableDataRef buffer = CFDataCreateMutable(kCFAllocatorDefault, 0);
TStringCollectionCallbackContext cx = {buffer, format};
CFDictionaryApplyFunction(dictionary, DictionaryBufferAppendCallback, &cx);
TStringIRep* rep = TSICTStringCreateWithDataOfTypeAndFormat(buffer, kTSITStringTagDict, format);
CFRelease(buffer);
CFRelease(dictionary);
return rep;
}
@@ -0,0 +1,74 @@
//
// TSICTString.h
// TSITString
//
// Created by Travis Tilley on 9/27/11.
//
#ifndef TSICTString_H
#define TSICTString_H
#include <CoreFoundation/CoreFoundation.h>
typedef enum {
kTSITStringTagString = 0,
kTSITStringTagNumber = 1,
kTSITStringTagFloat = 2,
kTSITStringTagBool = 3,
kTSITStringTagNull = 4,
kTSITStringTagDict = 5,
kTSITStringTagList = 6,
kTSITStringTagInvalid = 7,
} TSITStringTag;
extern const char* const TNetstringTypes;
extern const char* const OTNetstringTypes;
extern const UInt8 TNetstringSeparator;
typedef enum {
kTSITStringFormatDefault = 0,
kTSITStringFormatOTNetstring = 1,
kTSITStringFormatTNetstring = 2,
} TSITStringFormat;
extern TSITStringFormat TSITStringDefaultFormat;
typedef struct TSITStringIntermediate {
CFDataRef data;
char* length;
TSITStringTag type;
TSITStringFormat format;
} TStringIRep;
typedef struct {
CFMutableDataRef buffer;
TSITStringFormat format;
} TStringCollectionCallbackContext;
void Init_TSICTString(void);
void TSICTStringSetDefaultFormat(TSITStringFormat format);
TSITStringFormat TSICTStringGetDefaultFormat(void);
void TSICTStringDestroy(TStringIRep* rep);
CFDataRef TSICTStringCreateRenderedData(TStringIRep* rep);
CFDataRef TSICTStringCreateRenderedDataFromObjectWithFormat(CFTypeRef object, TSITStringFormat format);
CFStringRef TSICTStringCreateRenderedString(TStringIRep* rep);
CFStringRef TSICTStringCreateRenderedStringFromObjectWithFormat(CFTypeRef object, TSITStringFormat format);
TStringIRep* TSICTStringCreateWithObjectAndFormat(CFTypeRef object, TSITStringFormat format);
TStringIRep* TSICTStringCreateWithStringAndFormat(CFStringRef string, TSITStringFormat format);
TStringIRep* TSICTStringCreateWithNumberAndFormat(CFNumberRef number, TSITStringFormat format);
TStringIRep* TSICTStringCreateTrueWithFormat(TSITStringFormat format);
TStringIRep* TSICTStringCreateFalseWithFormat(TSITStringFormat format);
TStringIRep* TSICTStringCreateNullWithFormat(TSITStringFormat format);
TStringIRep* TSICTStringCreateInvalidWithFormat(TSITStringFormat format);
TStringIRep* TSICTStringCreateWithArrayAndFormat(CFArrayRef array, TSITStringFormat format);
TStringIRep* TSICTStringCreateWithDictionaryAndFormat(CFDictionaryRef dictionary, TSITStringFormat format);
#endif
@@ -0,0 +1,201 @@
#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 (classic, niw, \n"
" tnetstring, otnetstring)",
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 = kFSEventWatchOutputFormatOTNetstring;
}
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", CLI_NAME, CLI_VERSION);
#ifdef COMPILED_AT
printf("Compiled at: %s\n", COMPILED_AT);
#endif
#ifdef COMPILER
printf("Compiled with: %s\n", COMPILER);
#endif
#ifdef TARGET_CPU
printf("Compiled for: %s\n", TARGET_CPU);
#endif
printf("\n");
}
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
if (strcmp(optarg, "classic") == 0) {
args_info->format_arg = kFSEventWatchOutputFormatClassic;
} else if (strcmp(optarg, "niw") == 0) {
args_info->format_arg = kFSEventWatchOutputFormatNIW;
} else if (strcmp(optarg, "tnetstring") == 0) {
args_info->format_arg = kFSEventWatchOutputFormatTNetstring;
} else if (strcmp(optarg, "otnetstring") == 0) {
args_info->format_arg = kFSEventWatchOutputFormatOTNetstring;
} else {
fprintf(stderr, "Unknown output format: %s\n", optarg);
exit(EXIT_FAILURE);
}
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;
}
@@ -0,0 +1,45 @@
#ifndef CLI_H
#define CLI_H
#include "common.h"
#ifndef CLI_NAME
#define CLI_NAME "fsevent_watch"
#endif /* CLI_NAME */
#ifndef PROJECT_VERSION
#error "PROJECT_VERSION not set"
#endif /* PROJECT_VERSION */
#ifndef CLI_VERSION
#define CLI_VERSION _xstr(PROJECT_VERSION)
#endif /* CLI_VERSION */
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;
enum FSEventWatchOutputFormat 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 */
@@ -0,0 +1,22 @@
#ifndef fsevent_watch_common_h
#define fsevent_watch_common_h
#include <CoreFoundation/CoreFoundation.h>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#endif
#include <CoreServices/CoreServices.h>
#include <unistd.h>
#include "compat.h"
#include "defines.h"
#include "TSICTString.h"
enum FSEventWatchOutputFormat {
kFSEventWatchOutputFormatClassic,
kFSEventWatchOutputFormatNIW,
kFSEventWatchOutputFormatTNetstring,
kFSEventWatchOutputFormatOTNetstring
};
#endif /* fsevent_watch_common_h */
@@ -0,0 +1,41 @@
#include "compat.h"
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_6) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0)
FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf = 0x00000008;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_7) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0)
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 (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_9) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0)
FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf = 0x00000020;
FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent = 0x00080000;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_9_0)
FSEventStreamEventFlags kFSEventStreamEventFlagItemIsHardlink = 0x00100000;
FSEventStreamEventFlags kFSEventStreamEventFlagItemIsLastHardlink = 0x00200000;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_13) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_11_0)
FSEventStreamCreateFlags kFSEventStreamCreateFlagUseExtendedData = 0x00000040;
FSEventStreamEventFlags kFSEventStreamEventFlagItemCloned = 0x00400000;
#endif
@@ -0,0 +1,100 @@
/**
* @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 listen_fsevents_compat_h
#define listen_fsevents_compat_h
#ifndef __CORESERVICES__
#include <CoreServices/CoreServices.h>
#endif // __CORESERVICES__
#ifndef __AVAILABILITY__
#include <Availability.h>
#endif // __AVAILABILITY__
#ifndef __MAC_10_6
#define __MAC_10_6 1060
#endif
#ifndef __MAC_10_7
#define __MAC_10_7 1070
#endif
#ifndef __MAC_10_9
#define __MAC_10_9 1090
#endif
#ifndef __MAC_10_10
#define __MAC_10_10 101000
#endif
#ifndef __MAC_10_13
#define __MAC_10_13 101300
#endif
#ifndef __IPHONE_6_0
#define __IPHONE_6_0 60000
#endif
#ifndef __IPHONE_7_0
#define __IPHONE_7_0 70000
#endif
#ifndef __IPHONE_9_0
#define __IPHONE_9_0 90000
#endif
#ifndef __IPHONE_11_0
#define __IPHONE_11_0 110000
#endif
#ifdef __cplusplus
extern "C" {
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_6) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0)
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_7) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0)
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents;
extern FSEventStreamEventFlags kFSEventStreamEventFlagItemCreated,
kFSEventStreamEventFlagItemRemoved,
kFSEventStreamEventFlagItemInodeMetaMod,
kFSEventStreamEventFlagItemRenamed,
kFSEventStreamEventFlagItemModified,
kFSEventStreamEventFlagItemFinderInfoMod,
kFSEventStreamEventFlagItemChangeOwner,
kFSEventStreamEventFlagItemXattrMod,
kFSEventStreamEventFlagItemIsFile,
kFSEventStreamEventFlagItemIsDir,
kFSEventStreamEventFlagItemIsSymlink;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_9) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_7_0)
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf;
extern FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_9_0)
extern FSEventStreamEventFlags kFSEventStreamEventFlagItemIsHardlink,
kFSEventStreamEventFlagItemIsLastHardlink;
#endif
#if (defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_13) || \
(defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_11_0)
extern FSEventStreamCreateFlags kFSEventStreamCreateFlagUseExtendedData;
extern FSEventStreamEventFlags kFSEventStreamEventFlagItemCloned;
#endif
#ifdef __cplusplus
}
#endif
#endif // listen_fsevents_compat_h
@@ -0,0 +1,42 @@
#ifndef fsevent_watch_defines_h
#define fsevent_watch_defines_h
#define _str(s) #s
#define _xstr(s) _str(s)
#define COMPILED_AT __DATE__ " " __TIME__
#if defined (__clang__)
#define COMPILER "clang " __clang_version__
#elif defined (__GNUC__)
#define COMPILER "gcc " __VERSION__
#else
#define COMPILER "unknown"
#endif
#if defined(__ppc__)
#define TARGET_CPU "ppc"
#elif defined(__ppc64__)
#define TARGET_CPU "ppc64"
#elif defined(__i386__)
#define TARGET_CPU "i386"
#elif defined(__x86_64__)
#define TARGET_CPU "x86_64"
#elif defined(__arm64__)
#define TARGET_CPU "arm64"
#else
#define TARGET_CPU "unknown"
#endif
#define FLAG_CHECK(flags, flag) ((flags) & (flag))
#define FPRINTF_FLAG_CHECK(flags, flag, msg, fd) \
do { \
if (FLAG_CHECK(flags, flag)) { \
fprintf(fd, "%s", msg "\n"); } } \
while (0)
#define FLAG_CHECK_STDERR(flags, flag, msg) \
FPRINTF_FLAG_CHECK(flags, flag, msg, stderr)
#endif /* fsevent_watch_defines_h */
@@ -0,0 +1,548 @@
#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;
}
@@ -0,0 +1,66 @@
#include "signal_handlers.h"
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
#define PPID_ALARM_INTERVAL 2 // send SIGALRM every this seconds
static pid_t orig_ppid;
static void signal_handler(int _) {
exit(EXIT_FAILURE);
}
static void check_ppid(void) {
if (getppid() != orig_ppid) {
exit(EXIT_FAILURE);
}
}
static void check_stdout_open(void) {
if (fcntl(STDOUT_FILENO, F_GETFD) < 0) {
exit(EXIT_FAILURE);
}
}
static void alarm_handler(int _) {
check_ppid();
check_stdout_open();
alarm(PPID_ALARM_INTERVAL);
signal(SIGALRM, alarm_handler);
}
static void die(const char *msg) {
fprintf(stderr, "\nFATAL: %s\n", msg);
abort();
}
static void install_signal_handler(int sig, void (*handler)(int)) {
if (signal(sig, handler) == SIG_ERR) {
die("Could not install signal handler");
}
}
void install_signal_handlers(void) {
// check pipe is still connected
check_stdout_open();
// watch getppid() every PPID_ALARM_INTERVAL seconds
orig_ppid = getppid();
if (orig_ppid <= 1) {
die("prematurely zombied");
}
install_signal_handler(SIGALRM, alarm_handler);
alarm(PPID_ALARM_INTERVAL);
// be sure to exit on SIGHUP, SIGPIPE
install_signal_handler(SIGHUP, signal_handler);
install_signal_handler(SIGPIPE, signal_handler);
}
@@ -0,0 +1,16 @@
/**
* @headerfile signal_handlers.h
* Signal handlers to stop the zombie hordes
*
* Catch and handle signals better so that we die faster like a good meat puppet.
*/
#ifndef fsevent_watch_signal_handlers_h
#define fsevent_watch_signal_handlers_h
void install_signal_handlers(void);
#endif // fsevent_watch_signal_handlers_h
+231
View File
@@ -0,0 +1,231 @@
# -*- encoding: utf-8 -*-
require 'rubygems' unless defined?(Gem)
require 'pathname'
require 'date'
require 'time'
require 'rake/clean'
raise "unable to find xcodebuild" unless system('which', 'xcodebuild')
FSEVENT_WATCH_EXE_VERSION = '0.1.5'
$this_dir = Pathname.new(__FILE__).dirname.expand_path
$final_exe = $this_dir.parent.join('bin/fsevent_watch')
$src_dir = $this_dir.join('fsevent_watch')
$obj_dir = $this_dir.join('build')
SRC = Pathname.glob("#{$src_dir}/*.c")
OBJ = SRC.map {|s| $obj_dir.join("#{s.basename('.c')}.o")}
$now = DateTime.now.xmlschema rescue Time.now.xmlschema
$CC = ENV['CC'] || `which clang || which gcc`.strip
$CFLAGS = ENV['CFLAGS'] || '-fconstant-cfstrings -fasm-blocks -fstrict-aliasing -Wall'
$ARCHFLAGS = ENV['ARCHFLAGS'] || '-arch x86_64'
$DEFINES = "-DNS_BUILD_32_LIKE_64 -DNS_BLOCK_ASSERTIONS -DPROJECT_VERSION=#{FSEVENT_WATCH_EXE_VERSION}"
$GCC_C_LANGUAGE_STANDARD = ENV['GCC_C_LANGUAGE_STANDARD'] || 'gnu11'
# generic developer id name so it'll match correctly for anyone who has only
# one developer id in their keychain (not that I expect anyone else to bother)
$CODE_SIGN_IDENTITY = 'Developer ID Application'
$arch = `uname -m`.strip
$os_release = `uname -r`.strip
$BUILD_TRIPLE = "#{$arch}-apple-darwin#{$os_release}"
$CCVersion = `#{$CC} --version | head -n 1`.strip
CLEAN.include OBJ.map(&:to_s)
CLEAN.include $obj_dir.join('Info.plist').to_s
CLEAN.include $obj_dir.join('fsevent_watch').to_s
CLOBBER.include $final_exe.to_s
task :sw_vers do
$mac_product_version = `sw_vers -productVersion`.strip
$mac_build_version = `sw_vers -buildVersion`.strip
$MACOSX_DEPLOYMENT_TARGET = ENV['MACOSX_DEPLOYMENT_TARGET'] || $mac_product_version.sub(/\.\d*$/, '')
$CFLAGS = "#{$CFLAGS} -mmacosx-version-min=#{$MACOSX_DEPLOYMENT_TARGET}"
end
task :get_sdk_info => :sw_vers do
$SDK_INFO = {}
version_info = `xcodebuild -version -sdk macosx#{$MACOSX_DEPLOYMENT_TARGET}`
raise "invalid SDK" unless !!$?.exitstatus
version_info.strip.each_line do |line|
next if line.strip.empty?
next unless line.include?(':')
match = line.match(/([^:]*): (.*)/)
next unless match
$SDK_INFO[match[1]] = match[2]
end
end
task :debug => :sw_vers do
$DEFINES = "-DDEBUG #{$DEFINES}"
$CFLAGS = "#{$CFLAGS} -O0 -fno-omit-frame-pointer -g"
end
task :release => :sw_vers do
$DEFINES = "-DNDEBUG #{$DEFINES}"
$CFLAGS = "#{$CFLAGS} -Ofast"
end
desc 'configure build type depending on whether ENV var FWDEBUG is set'
task :set_build_type => :sw_vers do
if ENV['FWDEBUG']
Rake::Task[:debug].invoke
else
Rake::Task[:release].invoke
end
end
desc 'set build arch to ppc'
task :ppc do
$ARCHFLAGS = '-arch ppc'
end
desc 'set build arch to x86_64'
task :x86_64 do
$ARCHFLAGS = '-arch x86_64'
end
desc 'set build arch to i386'
task :x86 do
$ARCHFLAGS = '-arch i386'
end
desc 'set build arch to arm64'
task :arm64 do
$ARCHFLAGS = '-arch arm64'
end
task :setup_env => [:set_build_type, :sw_vers, :get_sdk_info]
directory $obj_dir.to_s
file $obj_dir.to_s => :setup_env
SRC.zip(OBJ).each do |source, object|
file object.to_s => [source.to_s, $obj_dir.to_s] do
cmd = [
$CC,
$ARCHFLAGS,
"-std=#{$GCC_C_LANGUAGE_STANDARD}",
$CFLAGS,
$DEFINES,
"-I#{$src_dir}",
'-isysroot',
$SDK_INFO['Path'],
'-c', source,
'-o', object
]
sh(cmd.map {|s| s.to_s}.join(' '))
end
end
file $obj_dir.join('Info.plist').to_s => [$obj_dir.to_s, :setup_env] do
File.open($obj_dir.join('Info.plist').to_s, 'w+') do |file|
indentation = ''
indent = lambda {|num| indentation = ' ' * num }
add = lambda {|str| file << "#{indentation}#{str}\n" }
key = lambda {|str| add["<key>#{str}</key>"] }
string = lambda {|str| add["<string>#{str}</string>"] }
add['<?xml version="1.0" encoding="UTF-8"?>']
add['<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">']
add['<plist version="1.0">']
indent[2]
add['<dict>']
indent[4]
key['CFBundleExecutable']
string['fsevent_watch']
key['CFBundleIdentifier']
string['com.teaspoonofinsanity.fsevent_watch']
key['CFBundleName']
string['fsevent_watch']
key['CFBundleDisplayName']
string['FSEvent Watch CLI']
key['NSHumanReadableCopyright']
string['Copyright (C) 2011-2017 Travis Tilley']
key['CFBundleVersion']
string["#{FSEVENT_WATCH_EXE_VERSION}"]
key['LSMinimumSystemVersion']
string["#{$MACOSX_DEPLOYMENT_TARGET}"]
key['DTSDKBuild']
string["#{$SDK_INFO['ProductBuildVersion']}"]
key['DTSDKName']
string["macosx#{$SDK_INFO['SDKVersion']}"]
key['DTSDKPath']
string["#{$SDK_INFO['Path']}"]
key['BuildMachineOSBuild']
string["#{$mac_build_version}"]
key['BuildMachineOSVersion']
string["#{$mac_product_version}"]
key['FSEWCompiledAt']
string["#{$now}"]
key['FSEWVersionInfoBuilder']
string["#{`whoami`.strip}"]
key['FSEWBuildTriple']
string["#{$BUILD_TRIPLE}"]
key['FSEWCC']
string["#{$CC}"]
key['FSEWCCVersion']
string["#{$CCVersion}"]
key['FSEWCFLAGS']
string["#{$CFLAGS}"]
indent[2]
add['</dict>']
indent[0]
add['</plist>']
end
end
desc 'generate an Info.plist used for code signing as well as embedding build settings into the resulting binary'
task :plist => $obj_dir.join('Info.plist').to_s
file $obj_dir.join('fsevent_watch').to_s => [$obj_dir.to_s, $obj_dir.join('Info.plist').to_s] + OBJ.map(&:to_s) do
cmd = [
$CC,
$ARCHFLAGS,
"-std=#{$GCC_C_LANGUAGE_STANDARD}",
$CFLAGS,
$DEFINES,
"-I#{$src_dir}",
'-isysroot',
$SDK_INFO['Path'],
'-framework CoreFoundation -framework CoreServices',
'-sectcreate __TEXT __info_plist',
$obj_dir.join('Info.plist')
] + OBJ + [
'-o', $obj_dir.join('fsevent_watch')
]
sh(cmd.map {|s| s.to_s}.join(' '))
end
desc 'compile and link build/fsevent_watch'
task :build => $obj_dir.join('fsevent_watch').to_s
desc 'codesign build/fsevent_watch binary'
task :codesign => :build do
sh "codesign -s '#{$CODE_SIGN_IDENTITY}' #{$obj_dir.join('fsevent_watch')}"
end
directory $this_dir.parent.join('bin')
desc 'replace bundled fsevent_watch binary with build/fsevent_watch'
task :replace_exe => [$this_dir.parent.join('bin'), :build] do
sh "mv #{$obj_dir.join('fsevent_watch')} #{$final_exe}"
end
task :default => [:replace_exe, :clean]
@@ -0,0 +1,85 @@
# Copyright (c) 2011 Konstantin Haase
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'stringio'
module OTNetstring
class Error < StandardError; end
class << self
def parse(io, encoding = 'internal', fallback_encoding = nil)
fallback_encoding = io.encoding if io.respond_to? :encoding
io = StringIO.new(io) if io.respond_to? :to_str
length, byte = String.new, nil
while byte.nil? || byte =~ /\d/
length << byte if byte
byte = io.read(1)
end
if length.size > 9
raise Error, "#{length} is longer than 9 digits"
elsif length !~ /\d+/
raise Error, "Expected '#{byte}' to be a digit"
end
length = Integer(length)
case byte
when '#' then Integer io.read(length)
when ',' then with_encoding io.read(length), encoding, fallback_encoding
when '~' then
raise Error, "nil has length of 0, #{length} given" unless length == 0
when '!' then io.read(length) == 'true'
when '[', '{'
array = []
start = io.pos
array << parse(io, encoding, fallback_encoding) while io.pos - start < length
raise Error, 'Nested element longer than container' if io.pos - start != length
byte == "{" ? Hash[*array] : array
else
raise Error, "Unknown type '#{byte}'"
end
end
def encode(obj, string_sep = ',')
case obj
when String then with_encoding "#{obj.bytesize}#{string_sep}#{obj}", "binary"
when Integer then encode(obj.inspect, '#')
when NilClass then "0~"
when Array then encode(obj.map { |e| encode(e) }.join, '[')
when Hash then encode(obj.map { |a,b| encode(a)+encode(b) }.join, '{')
when FalseClass, TrueClass then encode(obj.inspect, '!')
else raise Error, 'cannot encode %p' % obj
end
end
private
def with_encoding(str, encoding, fallback = nil)
return str unless str.respond_to? :encode
encoding = Encoding.find encoding if encoding.respond_to? :to_str
encoding ||= fallback
encoding ? str.encode(encoding) : str
rescue EncodingError
str.force_encoding(encoding)
end
end
end
@@ -0,0 +1,3 @@
# -*- encoding: utf-8 -*-
require 'rb-fsevent/fsevent'
require 'rb-fsevent/version'
@@ -0,0 +1,157 @@
# -*- encoding: utf-8 -*-
require 'otnetstring'
class FSEvent
class << self
class_eval <<-END
def root_path
"#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))}"
end
END
class_eval <<-END
def watcher_path
"#{File.join(FSEvent.root_path, 'bin', 'fsevent_watch')}"
end
END
end
attr_reader :paths, :callback
def initialize args = nil, &block
watch(args, &block) unless args.nil?
end
def watch(watch_paths, options=nil, &block)
@paths = watch_paths.kind_of?(Array) ? watch_paths : [watch_paths]
@callback = block
if options.kind_of?(Hash)
@options = parse_options(options)
elsif options.kind_of?(Array)
@options = options
else
@options = []
end
end
def run
@pipe = open_pipe
@running = true
# please note the use of IO::select() here, as it is used specifically to
# preserve correct signal handling behavior in ruby 1.8.
while @running && IO::select([@pipe], nil, nil, nil)
# managing the IO ourselves allows us to be careful and never pass an
# incomplete message to OTNetstring.parse()
message = String.new
length = String.new
byte = nil
reading_length = true
found_length = false
while reading_length
byte = @pipe.read_nonblock(1)
if "#{byte}" =~ /\d/
length << byte
found_length = true
elsif found_length == false
next
else
reading_length = false
end
end
length = Integer(length, 10)
type = byte
message << "#{length}#{type}"
message << @pipe.read(length)
decoded = OTNetstring.parse(message)
modified_paths = decoded["events"].map {|event| event["path"]}
# passing the full info as a second block param feels icky, but such is
# the trap of backward compatibility.
case callback.arity
when 1
callback.call(modified_paths)
when 2
callback.call(modified_paths, decoded)
end
end
rescue Interrupt, IOError, Errno::EBADF
ensure
stop
end
def stop
unless @pipe.nil?
Process.kill('KILL', @pipe.pid) if process_running?(@pipe.pid)
@pipe.close
end
rescue IOError, Errno::EBADF
ensure
@running = false
end
def process_running?(pid)
begin
Process.kill(0, pid)
true
rescue Errno::ESRCH
false
end
end
if RUBY_VERSION < '1.9'
def open_pipe
IO.popen("'#{self.class.watcher_path}' #{options_string} #{shellescaped_paths}")
end
private
def options_string
@options.join(' ')
end
def shellescaped_paths
@paths.map {|path| shellescape(path)}.join(' ')
end
# for Ruby 1.8.6 support
def shellescape(str)
# An empty argument will be skipped, so return empty quotes.
return "''" if str.empty?
str = str.dup
# Process as a single byte sequence because not all shell
# implementations are multibyte aware.
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
# A LF cannot be escaped with a backslash because a backslash + LF
# combo is regarded as line continuation and simply ignored.
str.gsub!(/\n/, "'\n'")
return str
end
else
def open_pipe
IO.popen([self.class.watcher_path] + @options + @paths)
end
end
private
def parse_options(options={})
opts = ['--format=otnetstring']
opts.concat(['--since-when', options[:since_when]]) if options[:since_when]
opts.concat(['--latency', options[:latency]]) if options[:latency]
opts.push('--no-defer') if options[:no_defer]
opts.push('--watch-root') if options[:watch_root]
opts.push('--file-events') if options[:file_events]
# ruby 1.9's IO.popen(array-of-stuff) syntax requires all items to be strings
opts.map {|opt| "#{opt}"}
end
end
@@ -0,0 +1,5 @@
# -*- encoding: utf-8 -*-
class FSEvent
VERSION = '0.11.2'
end
@@ -0,0 +1,26 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'rb-fsevent/version'
Gem::Specification.new do |s|
s.name = 'rb-fsevent'
s.version = FSEvent::VERSION
s.authors = ['Thibaud Guillaume-Gentil', 'Travis Tilley']
s.email = ['thibaud@thibaud.gg', 'ttilley@gmail.com']
s.homepage = 'http://rubygems.org/gems/rb-fsevent'
s.summary = 'Very simple & usable FSEvents API'
s.description = 'FSEvents API with Signals catching (without RubyCocoa)'
s.license = 'MIT'
s.metadata = {
'source_code_uri' => 'https://github.com/thibaudgg/rb-fsevent'
}
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
s.require_path = 'lib'
s.add_development_dependency 'rspec', '~> 3.6'
s.add_development_dependency 'guard-rspec', '~> 4.2'
s.add_development_dependency 'rake', '~> 12.0'
end