pbind 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1e4fc51921052509464b90b73e739278ea397f5
4
- data.tar.gz: 870f80bf09baa2fd496be63832384670021cdb6f
3
+ metadata.gz: 81e1f081ae44630a0c6fc01ddd9875ac5bc751ef
4
+ data.tar.gz: d12dfa76614ead7d6e29d8b90d1ff13f4d7382d3
5
5
  SHA512:
6
- metadata.gz: c6def51b03be6d197676e9f95e8e23f85c35eb70af527eb217c77a53500103cf80f922978ad4693d64823b732d57d0f89068e7cdbdd8f3c063d9df991e057c84
7
- data.tar.gz: 90b2e4bc7751110ccedad5bcf935cf01e59769984d9d87d5741c02831729f626130a4cf7f951f60571a54b0e92268030e99387775c96531eb2eaab6b61e597cd
6
+ metadata.gz: be3540ef369e882c103a469e87e76ba3463e4428c2371926e728683a0825308c562d9d54b9a57411ee390061ca060d8c70444d1535119088cc5a908ed4a00aa9
7
+ data.tar.gz: 62dd698fe2c428e4dfc06eddb24dc1791a476a4a1620ff898e45445a975a3c441dd11b191b46573d8da899f85adfed7bb945f868ffddf586ca9a9f78c867ab65
@@ -3,91 +3,4 @@ require 'xcodeproj'
3
3
 
4
4
  module Pbind
5
5
  require_relative 'pbind/command'
6
- end
7
-
8
- # class Pbind
9
-
10
- # install_dir = 'PBPlayground'
11
-
12
- # # project.targets.each do |target|
13
- # # puts target.name
14
- # # end
15
-
16
- # # target = project.targets.first
17
- # # files = target.source_build_phase.files.to_a.map do |pbx_build_file|
18
- # # pbx_build_file.file_ref.real_path.to_s
19
-
20
- # # end.select do |path|
21
- # # path.end_with?(".m", ".mm", ".swift")
22
-
23
- # # end.select do |path|
24
- # # File.exists?(path)
25
- # # end
26
-
27
- # # puts files
28
-
29
- # define_method :downloadSource do
30
- # if File.exists?(install_dir)
31
- # return true
32
- # end
33
-
34
- # `curl -OL https://github.com/wequick/Pbind/releases/download/0.6.0/PBPlayground.zip && tar -xvf PBPlayground.zip && rm PBPlayground.zip`
35
- # return File.exists?(install_dir)
36
- # end
37
-
38
- # define_method :addReferences do |project_path|
39
- # project = Xcodeproj::Project.open(project_path)
40
- # target = project.targets.first
41
-
42
- # group = project.main_group.find_subpath(install_dir, true)
43
- # group.set_source_tree('SOURCE_ROOT')
44
- # group.clear
45
-
46
- # file_refs = Array.new
47
- # Dir.foreach(install_dir) do |file|
48
- # if !File.directory?(file)
49
- # file_refs << group.new_reference(File.join(install_dir, file))
50
- # end
51
- # end
52
-
53
- # target.add_file_references(file_refs)
54
- # project.save
55
- # end
56
-
57
- # def logSuccess
58
- # puts "[ \e[32mOK\e[0m ]"
59
- # end
60
-
61
- # def logFailed
62
- # puts "[\e[31mFAILED\e[0m]"
63
- # end
64
-
65
- # define_method :install do
66
- # project_paths = Dir.glob("*.xcodeproj")
67
- # if project_paths.empty?
68
- # puts "Failed to find any xcodeproj!"
69
- # return
70
- # end
71
-
72
- # project_path = project_paths[0]
73
- # print '%-64s' % 'Downloading PBPlayground source...'
74
- # ret = downloadSource
75
- # if !ret
76
- # logFailed
77
- # return
78
- # end
79
- # logSuccess
80
-
81
- # print '%-64s' % 'Add PBPlayground reference to project...'
82
- # addReferences project_path
83
- # logSuccess
84
- # end
85
-
86
- # def self.hi
87
- # puts "Hello world!"
88
- # end
89
-
90
- # def self.exec argv
91
- # puts "Hello #{argv[0]}"
92
- # end
93
- # end
6
+ end
@@ -5,11 +5,12 @@ module Pbind
5
5
  class Command < CLAide::Command
6
6
 
7
7
  require_relative 'command/watch'
8
+ require_relative 'command/mock'
8
9
 
9
10
  self.abstract_command = true
10
11
  self.command = 'pbind'
11
12
  self.version = version
12
- self.description = 'Pbind, the Pbind xcode project helper.'
13
+ self.description = 'Pbind, the Pbind XcodeProject Helper.'
13
14
  self.plugin_prefixes = %w(claide pbind)
14
15
 
15
16
  def self.report_error(exception)
@@ -22,7 +23,7 @@ module Pbind
22
23
  raise
23
24
  else
24
25
  # if ENV['PBIND_ENV'] != 'development'
25
- # puts UI::ErrorReport.report(exception)
26
+ # puts UI::ErrorReport.report(exception)
26
27
  # UI::ErrorReport.search_for_exceptions(exception)
27
28
  # exit 1
28
29
  # else
@@ -31,5 +32,34 @@ module Pbind
31
32
  end
32
33
  end
33
34
 
35
+ def self.options
36
+ [
37
+ ['--project=path/to/Project.xcodeproj', 'The path of the XcodeProject.']
38
+ ].concat(super)
39
+ end
40
+
41
+ def initialize(argv)
42
+ super
43
+
44
+ @project_path = argv.option('project')
45
+ end
46
+
47
+ def verify_project_exists
48
+ if @project_path == nil
49
+ projects = Dir.glob("*.xcodeproj")
50
+ num_project = projects.length
51
+
52
+ help! 'No `*.xcodeproj\' found in the project directory.' if num_project == 0
53
+ help! "Could not automatically select an Xcode project. Specify one in your arguments like so:\
54
+ \n\n --project=path/to/Project.xcodeproj" unless num_project == 1
55
+
56
+ @project_path = projects[0]
57
+ else
58
+ help! 'The Xcode project should ends with `*.xcodeproj`.' unless @project_path.end_with?('.xcodeproj')
59
+ absolute_path = File.absolute_path(@project_path)
60
+ help! "Unable to find the Xcode project `#{absolute_path}`." unless File.exists?(absolute_path)
61
+ end
62
+ end
63
+
34
64
  end
35
65
  end
@@ -0,0 +1,109 @@
1
+ require 'xcodeproj'
2
+ require 'pathname'
3
+ require 'fileutils'
4
+
5
+ module Pbind
6
+ class Command
7
+ class Mock < Command
8
+ self.summary = 'Enable JSON mocking feature for Pbind.'
9
+ self.description = <<-DESC
10
+ Create `CLIENT`/`ACTION`.json under PBLocalhost directory for the XCODEPROJ.
11
+ DESC
12
+
13
+ self.arguments = [
14
+ CLAide::Argument.new(%(CLIENT ACTION), true),
15
+ ]
16
+
17
+ def initialize(argv)
18
+ super
19
+ @client = argv.shift_argument
20
+ @action = argv.shift_argument
21
+ end
22
+
23
+ def validate!
24
+ verify_project_exists
25
+ help! 'The client is required.' unless @client
26
+ help! 'The action is required.' unless @action
27
+ end
28
+
29
+ def run
30
+ @api_name = 'PBLocalhost'
31
+ @project_root = File.dirname(@project_path)
32
+ @api_install_dir = File.absolute_path(File.join(@project_root, @api_name))
33
+ @project = Xcodeproj::Project.open(@project_path)
34
+ @changed = false
35
+
36
+ add_mock_json
37
+
38
+ if !@changed
39
+ puts 'All are UP-TO-DATE.'
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ #----------------------------------------#
46
+
47
+ # !@group Private helpers
48
+
49
+ # Create [CLIENT]/[ACTION].json under PBLocalhost directory
50
+ #
51
+ # @return [void]
52
+ #
53
+ def add_mock_json
54
+ project = @project
55
+ target = project.targets.first
56
+ changed = false
57
+
58
+ # Add PBLocalhost group
59
+ group = project.main_group.find_subpath(@api_name, true)
60
+ if group.empty?
61
+ group.clear
62
+ file_refs = Array.new
63
+ Dir.foreach(@api_install_dir) do |file|
64
+ if !File.directory?(file)
65
+ file_refs << group.new_reference(File.join(@api_install_dir, file))
66
+ end
67
+ end
68
+ target.add_file_references(file_refs)
69
+ changed = true
70
+ end
71
+
72
+ # Create directory
73
+ client_dir = File.join(@api_install_dir, @client)
74
+ if !File.exists?(client_dir)
75
+ Dir.mkdir client_dir
76
+ changed = true
77
+ end
78
+
79
+ # Create json file
80
+ json_path = File.join(client_dir, "#{@action}.json")
81
+ if !File.exists?(json_path)
82
+ json_file = File.new(json_path, 'w')
83
+ json_file.puts("{\n \n}")
84
+ changed = true
85
+ end
86
+
87
+ # Add json file reference
88
+ group = group.find_subpath(@client, true)
89
+ if group.empty?
90
+ group.clear
91
+ file_refs = Array.new
92
+ file_refs << group.new_reference(json_path)
93
+ target.add_file_references(file_refs)
94
+ changed = true
95
+ end
96
+
97
+ # Save
98
+ if !changed
99
+ return
100
+ end
101
+
102
+ UI.section("Creating PBLocalhost/#{@client}/#{@action}.json.") do
103
+ project.save
104
+ @changed = true
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -5,33 +5,28 @@ require 'fileutils'
5
5
  module Pbind
6
6
  class Command
7
7
  class Watch < Command
8
- self.summary = 'Enable Pbind can watch sources and apply instant changes at runtime.'
8
+ self.abstract_command = false
9
+ self.summary = 'Enable the live load feature for Pbind.'
9
10
  self.description = <<-DESC
10
- 1. Download `PBPlayground` sources from GitHub, create a new group named `PBPlayground` and add it to the current project.
11
+ Downloads all dependencies of `PBLiveLoader` for the Xcode project.
11
12
 
12
- 2. Modify the project `Info.plist` and set `PBResourcesPath` to `$(SRCROOT)/[project_name]`, `PBLocalhost` to `$(SRCROOT)/PBLocalhost`.
13
+ The Xcode project file should be specified in `--project` like this:
13
14
 
14
- 3. At runtime: `[PBPlayground load]` observe the application lifecycle and watch the above paths to apply instant changes.
15
- DESC
15
+ --project=path/to/Project.xcodeproj
16
16
 
17
- self.arguments = [
18
- CLAide::Argument.new('XCODEPROJ', true),
19
- ]
17
+ If no project is specified, then a search for an Xcode project will be made. If
18
+ more than one Xcode project is found, the command will raise an error.
20
19
 
21
- def initialize(argv)
22
- @project_path = argv.shift_argument
23
- end
20
+ This will configure the project to reference the Pbind LiveLoader library
21
+ and add a PBLocalhost directory for later JSON mocking.
22
+ DESC
24
23
 
25
24
  def validate!
26
- # super
27
- help! 'A path for the xcodeproj is required.' unless @project_path
28
- @project_path = @project_path.chomp('/')
29
- help! 'The xcodeproj path should ends with ".xcodeproj"' unless @project_path.end_with?('.xcodeproj')
30
- help! "The xcodeproj path '#{@project_path}' is not exists." unless File.exists?(@project_path)
25
+ verify_project_exists
31
26
  end
32
27
 
33
28
  def run
34
- @src_name = 'PBPlayground'
29
+ @src_name = 'PBLiveLoader'
35
30
  @api_name = 'PBLocalhost'
36
31
  @src_key = 'PBResourcesPath'
37
32
  @project_root = File.dirname(@project_path)
@@ -55,7 +50,7 @@ module Pbind
55
50
 
56
51
  # !@group Private helpers
57
52
 
58
- # Install the `PBPlayground`, `PBLocalhost` sources
53
+ # Install the `PBLiveLoader`, `PBLocalhost` sources
59
54
  #
60
55
  # @return [void]
61
56
  #
@@ -65,7 +60,7 @@ module Pbind
65
60
  end
66
61
 
67
62
  source_dir = ENV['PBIND_SOURCE']
68
- UI.section("Copying `#{source_dir}` into `#{@project_root}`.") do
63
+ UI.section("Copying [`#{@src_name}`, `#{@api_name}`] into `#{@project_root}`.") do
69
64
  FileUtils.cp_r File.join(source_dir, @src_name), @project_root
70
65
  FileUtils.cp_r File.join(source_dir, @api_name), @project_root
71
66
  @changed = true
@@ -113,7 +108,7 @@ module Pbind
113
108
  end
114
109
  end
115
110
 
116
- # Add `PBPlayground`, `PBLocalhost` group references to the project
111
+ # Add `PBLiveLoader`, `PBLocalhost` group references to the project
117
112
  #
118
113
  # @return [Bool] something changed
119
114
  #
@@ -122,7 +117,7 @@ module Pbind
122
117
  target = project.targets.first
123
118
  changed = false
124
119
 
125
- # Add PBPlayground group
120
+ # Add PBLiveLoader group
126
121
  group = project.main_group.find_subpath(@src_name, true)
127
122
  if group.empty?
128
123
  group.set_source_tree('SOURCE_ROOT')
@@ -0,0 +1,38 @@
1
+ //
2
+ // PBDirectoryWatcher.h
3
+ // Pbind
4
+ //
5
+ // Created by Galen Lin on 2016/12/9.
6
+ //
7
+
8
+ #include <targetconditionals.h>
9
+
10
+ #if (DEBUG && TARGET_IPHONE_SIMULATOR)
11
+
12
+ #import <Foundation/Foundation.h>
13
+
14
+ typedef enum : NSUInteger {
15
+ PBDirEventNewFolder,
16
+ PBDirEventNewFile,
17
+ PBDirEventDeleteFolder,
18
+ PBDirEventDeleteFile,
19
+ PBDirEventModifyFile
20
+ } PBDirEvent;
21
+
22
+ /**
23
+ A watcher that recursively watch directories|files create|modify|delete events.
24
+ */
25
+ @interface PBDirectoryWatcher : NSObject
26
+
27
+ /**
28
+ Start watching the directory.
29
+
30
+ @param dir the watching directory root.
31
+ @param handler the handler to handle all the events.
32
+ */
33
+ - (void)watchDir:(NSString *)dir
34
+ handler:(void (^)(NSString *path, BOOL initial, PBDirEvent event))handler;
35
+
36
+ @end
37
+
38
+ #endif
@@ -0,0 +1,466 @@
1
+ //
2
+ // PBDirectoryWatcher.m
3
+ // Pbind
4
+ //
5
+ // Created by Galen Lin on 2016/12/9.
6
+ //
7
+
8
+ #import "PBDirectoryWatcher.h"
9
+
10
+ #if (DEBUG && TARGET_IPHONE_SIMULATOR)
11
+
12
+ #include <stdio.h>
13
+ #include <string.h>
14
+ #include <sys/types.h>
15
+ #include <dirent.h>
16
+ #include <sys/stat.h>
17
+
18
+ #include <fcntl.h>
19
+ #include <unistd.h>
20
+ #include <sys/event.h>
21
+
22
+ typedef void(^file_handler)(const char *dir, const char *file, const char *name, BOOL isDir);
23
+ static int walk_dir(const char *path, int depth_limit, file_handler handler);
24
+
25
+ //
26
+
27
+ @interface _PBMonitorFile : NSObject
28
+
29
+ @property (nonatomic, strong) NSString *path;
30
+ @property (nonatomic, assign) NSTimeInterval lastModified;
31
+
32
+ - (instancetype)initWithPath:(NSString *)path;
33
+ - (BOOL)updateLastModified;
34
+
35
+ @end
36
+
37
+ //
38
+
39
+ @class _PBDirWatcher;
40
+ @protocol _PBDirWatcherDelegate <NSObject>
41
+
42
+ - (void)directoryDidChange:(_PBDirWatcher *)watcher;
43
+
44
+ @end
45
+
46
+ //
47
+
48
+ @interface _PBDirWatcher : NSObject
49
+
50
+ @property (nonatomic, readonly) NSString *path;
51
+ @property (copy, nonatomic) void (^update)(void);
52
+
53
+ - (instancetype)initWithDir:(NSString *)dir file:(NSString *)file delegate:(id<_PBDirWatcherDelegate>)delegate;
54
+
55
+ - (void)addSubdir:(NSString *)dir;
56
+ - (void)addFile:(NSString *)file;
57
+ - (_PBMonitorFile *)fileForPath:(NSString *)path;
58
+ - (NSArray<NSString *> *)updateSubdirs:(NSArray<NSString *> *)subdirs;
59
+ - (NSArray<NSString *> *)updateFiles:(NSArray<NSString *> *)subdirs;
60
+
61
+ - (void)invalidate;
62
+
63
+ @end
64
+
65
+
66
+ @interface PBDirectoryWatcher () <_PBDirWatcherDelegate>
67
+ {
68
+ NSMutableArray<_PBDirWatcher *> *_dirWatchers;
69
+ NSArray<NSString *> *_extensions;
70
+ void (^_handler)(NSString *path, BOOL initial, PBDirEvent event);
71
+ }
72
+
73
+ @end
74
+
75
+ @implementation PBDirectoryWatcher
76
+
77
+ - (void)watchDir:(NSString *)dir
78
+ handler:(void (^)(NSString *path, BOOL initial, PBDirEvent event))handler
79
+ {
80
+ _dirWatchers = [[NSMutableArray alloc] init];
81
+ _handler = handler;
82
+ [self scan:dir watcher:nil];
83
+ }
84
+
85
+ - (void)scan:(NSString *)path watcher:(_PBDirWatcher *)watcher
86
+ {
87
+ BOOL initial = (watcher == nil);
88
+ int walk_depth = initial ? 0 : 1;
89
+
90
+ NSMutableArray *subdirs;
91
+ NSMutableArray *files;
92
+ if (!initial) {
93
+ subdirs = [[NSMutableArray alloc] init];
94
+ files = [[NSMutableArray alloc] init];
95
+ }
96
+
97
+ walk_dir([path UTF8String], walk_depth, ^(const char *dir, const char *file, const char *name, BOOL isDir) {
98
+ if (isDir) {
99
+ NSString *resDir = [[NSString alloc] initWithUTF8String:file];
100
+ NSString *parentDir = [[NSString alloc] initWithUTF8String:dir];
101
+ NSArray *filteredWatchers = [_dirWatchers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path == %@", parentDir]];
102
+ if (filteredWatchers.count == 1) {
103
+ _PBDirWatcher *parentWatcher = filteredWatchers[0];
104
+ [parentWatcher addSubdir:resDir];
105
+ }
106
+
107
+ filteredWatchers = [_dirWatchers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path == %@", resDir]];
108
+ if (filteredWatchers.count == 0) {
109
+ // New dir
110
+ _PBDirWatcher *dirMonitor = [[_PBDirWatcher alloc] initWithDir:resDir file:nil delegate:self];
111
+ [_dirWatchers addObject:dirMonitor];
112
+ _handler(resDir, initial, PBDirEventNewFolder);
113
+ }
114
+
115
+ if (!initial) {
116
+ [subdirs addObject:resDir];
117
+ }
118
+
119
+ return;
120
+ }
121
+
122
+ NSString *resDir = [[NSString alloc] initWithUTF8String:dir];
123
+ NSString *resFilePath = [[NSString alloc] initWithUTF8String:file];
124
+ NSArray *filteredWatchers = [_dirWatchers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path == %@", resDir]];
125
+ _PBDirWatcher *dirWatcher;
126
+ if (filteredWatchers.count == 0) {
127
+ // New dir to watch
128
+ dirWatcher = [[_PBDirWatcher alloc] initWithDir:resDir file:resFilePath delegate:self];
129
+ [_dirWatchers addObject:dirWatcher];
130
+ _handler(resDir, initial, PBDirEventNewFolder);
131
+ _handler(resFilePath, initial, PBDirEventNewFile); // notify change
132
+ } else {
133
+ // Old dir had watch
134
+ dirWatcher = filteredWatchers[0];
135
+ _PBMonitorFile *resFile = [dirWatcher fileForPath:resFilePath];
136
+ if (resFile == nil) {
137
+ // New file
138
+ [dirWatcher addFile:resFilePath];
139
+
140
+ _handler(resFilePath, initial, PBDirEventNewFile); // notify change
141
+ } else {
142
+ // Old file
143
+ if ([resFile updateLastModified]) {
144
+ // Modified
145
+
146
+ _handler(resFilePath, initial, PBDirEventModifyFile); // notify change
147
+ }
148
+ }
149
+ }
150
+
151
+ if (!initial) {
152
+ [files addObject:resFilePath];
153
+ }
154
+ });
155
+
156
+ if (!initial) {
157
+ // Check if any directory be deleted.
158
+ NSArray *deletedDirs = [watcher updateSubdirs:subdirs];
159
+ if (deletedDirs != nil) {
160
+ // Unwatch
161
+ NSArray *filters = [_dirWatchers filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path in %@", deletedDirs]];
162
+ for (_PBDirWatcher *watcher in filters) {
163
+ [watcher invalidate];
164
+ }
165
+ [_dirWatchers removeObjectsInArray:filters];
166
+
167
+ // Notify change
168
+ for (NSString *dir in deletedDirs) {
169
+ _handler(dir, initial, PBDirEventDeleteFolder);
170
+ }
171
+ }
172
+
173
+ // Check if any file be deleted.
174
+ NSArray *deletedFiles = [watcher updateFiles:files];
175
+ if (deletedFiles != nil) {
176
+ for (NSString *file in deletedFiles) {
177
+ _handler(file, initial, PBDirEventDeleteFile);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ - (void)directoryDidChange:(_PBDirWatcher *)watcher {
184
+ [self scan:watcher.path watcher:watcher];
185
+ }
186
+
187
+ - (void)dealloc
188
+ {
189
+ _dirWatchers = nil;
190
+ _handler = nil;
191
+ }
192
+
193
+ @end
194
+
195
+ //
196
+
197
+ @implementation _PBMonitorFile
198
+
199
+ - (instancetype)initWithPath:(NSString *)path
200
+ {
201
+ if (self = [super init]) {
202
+ _path = path;
203
+ _lastModified = [self currentLastModified];
204
+ }
205
+ return self;
206
+ }
207
+
208
+ - (BOOL)updateLastModified
209
+ {
210
+ NSTimeInterval currentLastModified = [self currentLastModified];
211
+ if (_lastModified != currentLastModified) {
212
+ _lastModified = currentLastModified;
213
+ return YES;
214
+ }
215
+ return NO;
216
+ }
217
+
218
+ - (NSTimeInterval)currentLastModified
219
+ {
220
+ NSError *error = nil;
221
+ NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:_path error:&error];
222
+ NSTimeInterval lastModified = 0;
223
+ if (error == nil) {
224
+ lastModified = [[attrs fileModificationDate] timeIntervalSince1970];
225
+ }
226
+ return lastModified;
227
+ }
228
+
229
+ @end
230
+
231
+ // Directory watcher
232
+ // @see https://developer.apple.com/library/content/samplecode/DocInteraction/Listings/Classes_DirectoryWatcher_m.html
233
+
234
+ @implementation _PBDirWatcher {
235
+ int kq;
236
+ int dirFD;
237
+ CFFileDescriptorRef dirKQRef;
238
+ __weak id<_PBDirWatcherDelegate> delegate;
239
+ NSMutableArray<NSString *> *subdirs;
240
+ NSMutableArray<_PBMonitorFile *> *files;
241
+ }
242
+
243
+ static void KQCallback(CFFileDescriptorRef kqRef, CFOptionFlags callBackTypes, void *info) {
244
+ _PBDirWatcher* obj = (__bridge _PBDirWatcher*) info;
245
+
246
+ assert([obj isKindOfClass:[_PBDirWatcher class]]);
247
+ assert(kqRef == obj->dirKQRef);
248
+ assert(callBackTypes == kCFFileDescriptorReadCallBack);
249
+
250
+ [obj kqueueFired];
251
+ }
252
+
253
+ - (instancetype)initWithDir:(NSString *)dir file:(NSString *)file delegate:(id<_PBDirWatcherDelegate>)aDelegate
254
+ {
255
+ if (self = [super init]) {
256
+ kq = -1;
257
+ dirFD = -1;
258
+ delegate = aDelegate;
259
+
260
+ _path = dir;
261
+ files = [[NSMutableArray alloc] init];
262
+ if (file != nil) {
263
+ _PBMonitorFile *resFile = [[_PBMonitorFile alloc] initWithPath:file];
264
+ [files addObject:resFile];
265
+ }
266
+ [self start];
267
+ }
268
+ return self;
269
+ }
270
+
271
+ - (void)addSubdir:(NSString *)dir
272
+ {
273
+ if (subdirs == nil) {
274
+ subdirs = [[NSMutableArray alloc] init];
275
+ }
276
+ [subdirs addObject:dir];
277
+ }
278
+
279
+ - (void)addFile:(NSString *)file
280
+ {
281
+ _PBMonitorFile *resFile = [[_PBMonitorFile alloc] initWithPath:file];
282
+ [files addObject:resFile];
283
+ }
284
+
285
+ - (_PBMonitorFile *)fileForPath:(NSString *)path
286
+ {
287
+ NSArray *filteredFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"path==%@", path]];
288
+ if (filteredFiles.count == 0) {
289
+ return nil;
290
+ }
291
+ return filteredFiles[0];
292
+ }
293
+
294
+ - (NSArray<NSString *> *)updateSubdirs:(NSArray<NSString *> *)the_subdirs
295
+ {
296
+ if (subdirs == nil) return nil;
297
+
298
+ NSArray *filters = [subdirs filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT SELF IN %@", the_subdirs]];
299
+ if (filters.count > 0) {
300
+ [subdirs removeObjectsInArray:filters];
301
+ return filters;
302
+ }
303
+
304
+ return nil;
305
+ }
306
+
307
+ - (NSArray<NSString *> *)updateFiles:(NSArray<NSString *> *)the_files
308
+ {
309
+ if (files == nil) return nil;
310
+
311
+ NSArray *filters = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"NOT path IN %@", the_files]];
312
+ if (filters.count > 0) {
313
+ [files removeObjectsInArray:filters];
314
+ return [filters valueForKey:@"path"];
315
+ }
316
+
317
+ return nil;
318
+ }
319
+
320
+ - (void)dealloc {
321
+ [self invalidate];
322
+ }
323
+
324
+ - (void)kqueueFired {
325
+ assert(kq >= 0);
326
+
327
+ struct kevent event;
328
+ struct timespec timeout = {0, 0};
329
+ int eventCount;
330
+
331
+ eventCount = kevent(kq, NULL, 0, &event, 1, &timeout);
332
+ assert((eventCount >= 0) && (eventCount < 2));
333
+
334
+ // call our delegate of the directory change
335
+ [delegate directoryDidChange:self];
336
+
337
+ CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack);
338
+ }
339
+
340
+ - (void)start {
341
+ // Double initializing is not going to work...
342
+ if (dirKQRef != NULL) return;
343
+ if (dirFD != -1) return;
344
+ if (kq != -1) return;
345
+
346
+ if (_path == nil) return;
347
+
348
+ // Open the directory we're going to watch
349
+ dirFD = open([_path fileSystemRepresentation], O_EVTONLY);
350
+ if (dirFD < 0) return;
351
+
352
+ // Create a kqueue for our event messages...
353
+ kq = kqueue();
354
+ if (kq < 0) {
355
+ close(dirFD);
356
+ return;
357
+ }
358
+
359
+ struct kevent eventToAdd;
360
+ eventToAdd.ident = dirFD;
361
+ eventToAdd.filter = EVFILT_VNODE;
362
+ eventToAdd.flags = EV_ADD | EV_CLEAR;
363
+ eventToAdd.fflags = NOTE_WRITE;
364
+ eventToAdd.data = 0;
365
+ eventToAdd.udata = NULL;
366
+
367
+ int errNum = kevent(kq, &eventToAdd, 1, NULL, 0, NULL);
368
+ if (errNum != 0) {
369
+ close(kq);
370
+ close(dirFD);
371
+ return;
372
+ }
373
+
374
+ // Passing true in the third argument so CFFileDescriptorInvalidate will close kq.
375
+ CFFileDescriptorContext context = { 0, (__bridge void *)(self), NULL, NULL, NULL };
376
+ dirKQRef = CFFileDescriptorCreate(NULL, kq, true, KQCallback, &context);
377
+ if (dirKQRef == NULL) {
378
+ close(kq);
379
+ close(dirFD);
380
+ return;
381
+ }
382
+
383
+ // Spin out a pluggable run loop source from the CFFileDescriptorRef
384
+ // Add it to the current run loop, then release it
385
+ CFRunLoopSourceRef rls = CFFileDescriptorCreateRunLoopSource(NULL, dirKQRef, 0);
386
+ if (rls == NULL) {
387
+ CFRelease(dirKQRef);
388
+ close(kq);
389
+ close(dirFD);
390
+ dirKQRef = NULL;
391
+ return;
392
+ }
393
+
394
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
395
+ CFRelease(rls);
396
+ CFFileDescriptorEnableCallBacks(dirKQRef, kCFFileDescriptorReadCallBack);
397
+ }
398
+
399
+ - (void)invalidate
400
+ {
401
+ if (dirKQRef != NULL) {
402
+ CFFileDescriptorInvalidate(dirKQRef);
403
+ CFRelease(dirKQRef);
404
+ dirKQRef = NULL;
405
+ // We don't need to close the kq, CFFileDescriptorInvalidate closed it instead.
406
+ // Change the value so no one thinks it's still live.
407
+ kq = -1;
408
+ }
409
+
410
+ if (dirFD != -1) {
411
+ close(dirFD);
412
+ dirFD = -1;
413
+ }
414
+ }
415
+
416
+ @end
417
+
418
+ #pragma mark -
419
+ #pragma mark - Directory walker with pure C.
420
+
421
+ static int walk_dir_sub(const char *path, int depth_limit, int depth, file_handler handler) {
422
+ DIR *d;
423
+ struct dirent *file;
424
+ struct stat sb;
425
+ char current[256];
426
+ int next_depth = depth + 1;
427
+
428
+ if (!(d = opendir(path))) {
429
+ NSLog(@"error opendir %s!", path);
430
+ return -1;
431
+ }
432
+
433
+ while ((file = readdir(d)) != NULL) {
434
+ const char *name = file->d_name;
435
+ if (*name == '.') {
436
+ continue;
437
+ }
438
+
439
+ sprintf(current, "%s/%s", path, name);
440
+ if (stat(current, &sb) < 0) {
441
+ continue;
442
+ }
443
+
444
+ if (S_ISDIR(sb.st_mode)) {
445
+ handler(path, current, name, true);
446
+ if (depth_limit == next_depth) {
447
+ continue;
448
+ }
449
+
450
+ walk_dir_sub(current, depth_limit, next_depth, handler);
451
+ continue;
452
+ }
453
+
454
+ handler(path, current, name, false);
455
+ }
456
+
457
+ closedir(d);
458
+
459
+ return 0;
460
+ }
461
+
462
+ static int walk_dir(const char *path, int depth_limit, file_handler handler) {
463
+ return walk_dir_sub(path, depth_limit, 0, handler);
464
+ }
465
+
466
+ #endif