kicker 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.kick +20 -0
- data/LICENSE +54 -0
- data/README.rdoc +141 -0
- data/Rakefile +37 -0
- data/TODO.rdoc +1 -0
- data/VERSION.yml +4 -0
- data/bin/kicker +5 -0
- data/html/images/kikker.jpg +0 -0
- data/kicker.gemspec +95 -0
- data/lib/kicker.rb +135 -0
- data/lib/kicker/callback_chain.rb +77 -0
- data/lib/kicker/core_ext.rb +30 -0
- data/lib/kicker/growl.rb +24 -0
- data/lib/kicker/options.rb +49 -0
- data/lib/kicker/recipes/could_not_handle_file.rb +5 -0
- data/lib/kicker/recipes/dot_kick.rb +35 -0
- data/lib/kicker/recipes/execute_cli_command.rb +6 -0
- data/lib/kicker/recipes/ignore.rb +39 -0
- data/lib/kicker/recipes/jstest.rb +8 -0
- data/lib/kicker/recipes/rails.rb +54 -0
- data/lib/kicker/utils.rb +71 -0
- data/lib/kicker/validate.rb +24 -0
- data/test/callback_chain_test.rb +150 -0
- data/test/core_ext_test.rb +28 -0
- data/test/filesystem_change_test.rb +100 -0
- data/test/fixtures/a_file_thats_reloaded.rb +2 -0
- data/test/initialization_test.rb +165 -0
- data/test/options_test.rb +30 -0
- data/test/recipes/could_not_handle_file_test.rb +11 -0
- data/test/recipes/dot_kick_test.rb +26 -0
- data/test/recipes/execute_cli_command_test.rb +32 -0
- data/test/recipes/ignore_test.rb +29 -0
- data/test/recipes/jstest_test.rb +31 -0
- data/test/recipes/rails_test.rb +73 -0
- data/test/test_helper.rb +6 -0
- data/test/utils_test.rb +123 -0
- data/vendor/growlnotifier/growl.rb +170 -0
- data/vendor/growlnotifier/growl_helpers.rb +25 -0
- data/vendor/rucola/fsevents.rb +136 -0
- metadata +110 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
before = Kicker.process_chain.dup
|
4
|
+
require 'kicker/recipes/rails'
|
5
|
+
RAILS = (Kicker.process_chain - before).first
|
6
|
+
|
7
|
+
describe "The Rails helper module" do
|
8
|
+
it "should return all functional tests" do
|
9
|
+
Dir.expects(:glob).with("test/functional/**/*_test.rb").returns(%w{ test.rb })
|
10
|
+
Rails.all_functional_tests.should == %w{ test.rb }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "The rails handler" do
|
15
|
+
before do
|
16
|
+
@files = %w{ Rakefile }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should match any test case files" do
|
20
|
+
should_match %w{ test/1_test.rb test/namespace/2_test.rb },
|
21
|
+
%w{ test/1_test.rb test/namespace/2_test.rb }
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should map model files to test/unit" do
|
25
|
+
should_match %w{ app/models/member.rb app/models/article.rb },
|
26
|
+
%w{ test/unit/member_test.rb test/unit/article_test.rb }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should map concern files to test/unit/concerns" do
|
30
|
+
should_match %w{ app/concerns/authenticate.rb app/concerns/nested_resource.rb },
|
31
|
+
%w{ test/unit/concerns/authenticate_test.rb test/unit/concerns/nested_resource_test.rb }
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should map helper files to test/unit/helpers" do
|
35
|
+
should_match %w{ app/helpers/members_helper.rb app/helpers/articles_helper.rb },
|
36
|
+
%w{ test/unit/helpers/members_helper_test.rb test/unit/helpers/articles_helper_test.rb }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should map controller files to test/functional" do
|
40
|
+
should_match %w{ app/controllers/application_controller.rb app/controllers/members_controller.rb },
|
41
|
+
%w{ test/functional/application_controller_test.rb test/functional/members_controller_test.rb }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should map view templates to test/functional" do
|
45
|
+
should_match %w{ app/views/members/index.html.erb app/views/admin/articles/show.html.erb },
|
46
|
+
%w{ test/functional/members_controller_test.rb test/functional/admin/articles_controller_test.rb }
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should run all functional tests when config/routes.rb is saved" do
|
50
|
+
tests = %w{ test/functional/members_controller_test.rb test/functional/admin/articles_controller_test.rb }
|
51
|
+
Rails.expects(:all_functional_tests).returns(tests)
|
52
|
+
should_match %w{ config/routes.rb }, tests
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should map lib files to test/lib" do
|
56
|
+
should_match %w{ lib/money.rb lib/views/date.rb },
|
57
|
+
%w{ test/lib/money_test.rb test/lib/views/date_test.rb }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def should_match(files, tests)
|
63
|
+
@files += files
|
64
|
+
|
65
|
+
tests.each do |test|
|
66
|
+
File.stubs(:exist?).with(test).returns(true)
|
67
|
+
end
|
68
|
+
|
69
|
+
Kicker::Utils.expects(:run_ruby_tests).with(tests)
|
70
|
+
RAILS.call(@files)
|
71
|
+
@files.should == %w{ Rakefile }
|
72
|
+
end
|
73
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/utils_test.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "A Kicker instance, concerning its utility methods" do
|
4
|
+
before do
|
5
|
+
Kicker.stubs(:growl)
|
6
|
+
utils.stubs(:last_command_succeeded?).returns(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should print a log entry with timestamp" do
|
10
|
+
now = Time.now
|
11
|
+
Time.stubs(:now).returns(now)
|
12
|
+
|
13
|
+
utils.expects(:puts).with("[#{now}] the message")
|
14
|
+
utils.send(:log, 'the message')
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should log the output of the command indented by 2 spaces and whether or not the command succeeded" do
|
18
|
+
utils.stubs(:`).returns("line 1\nline 2")
|
19
|
+
|
20
|
+
utils.expects(:log).with('Change occured, executing command: ls')
|
21
|
+
utils.expects(:log).with(' line 1')
|
22
|
+
utils.expects(:log).with(' line 2')
|
23
|
+
utils.expects(:log).with('Command succeeded')
|
24
|
+
utils.execute('ls')
|
25
|
+
|
26
|
+
utils.stubs(:last_command_succeeded?).returns(false)
|
27
|
+
utils.stubs(:last_command_status).returns(123)
|
28
|
+
utils.expects(:log).with('Change occured, executing command: ls')
|
29
|
+
utils.expects(:log).with(' line 1')
|
30
|
+
utils.expects(:log).with(' line 2')
|
31
|
+
utils.expects(:log).with('Command failed (123)')
|
32
|
+
utils.execute('ls')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should send the Growl messages with the default click callback" do
|
36
|
+
utils.stubs(:log)
|
37
|
+
|
38
|
+
utils.stubs(:`).returns("line 1\nline 2")
|
39
|
+
Kicker.use_growl = true
|
40
|
+
|
41
|
+
OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(2)
|
42
|
+
|
43
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
|
44
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
|
45
|
+
utils.execute('ls')
|
46
|
+
|
47
|
+
utils.stubs(:last_command_succeeded?).returns(false)
|
48
|
+
utils.stubs(:last_command_status).returns(123)
|
49
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
|
50
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
|
51
|
+
utils.execute('ls')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should send the Growl messages with a click callback which executes the specified growl command when succeeded" do
|
55
|
+
utils.stubs(:log)
|
56
|
+
|
57
|
+
utils.stubs(:`).returns("line 1\nline 2")
|
58
|
+
Kicker.use_growl = true
|
59
|
+
Kicker.growl_command = 'ls -l'
|
60
|
+
|
61
|
+
utils.expects(:system).with('ls -l').times(1)
|
62
|
+
OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(1)
|
63
|
+
|
64
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
|
65
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
|
66
|
+
utils.execute('ls')
|
67
|
+
|
68
|
+
utils.stubs(:last_command_succeeded?).returns(false)
|
69
|
+
utils.stubs(:last_command_status).returns(123)
|
70
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', 'ls')
|
71
|
+
Kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
|
72
|
+
utils.execute('ls')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should store the last executed command" do
|
76
|
+
utils.stubs(:log)
|
77
|
+
utils.execute('date')
|
78
|
+
utils.last_command.should == 'date'
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def utils
|
84
|
+
Kicker::Utils
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "Kernel utility methods" do
|
89
|
+
before do
|
90
|
+
utils.stubs(:last_command_succeeded?).returns(true)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should forward log calls to the Kicker::Utils module" do
|
94
|
+
utils.expects(:log).with('the message')
|
95
|
+
log 'the message'
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should forward execute calls to the Kicker::Utils module" do
|
99
|
+
utils.expects(:execute).with('ls')
|
100
|
+
execute 'ls'
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should return the last_command" do
|
104
|
+
utils.stubs(:last_command).returns('abcde')
|
105
|
+
last_command.should == 'abcde'
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should call execute with the appropriate command to execute Ruby tests" do
|
109
|
+
utils.expects(:execute).with("ruby -r test/1.rb -r test/2.rb -e ''")
|
110
|
+
run_ruby_tests %w{ test/1.rb test/2.rb }
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should not execute anything if an empty array is given to run_ruby_tests" do
|
114
|
+
utils.expects(:execute).never
|
115
|
+
run_ruby_tests []
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def utils
|
121
|
+
Kicker::Utils
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'osx/cocoa'
|
2
|
+
|
3
|
+
module Growl
|
4
|
+
class Notifier < OSX::NSObject
|
5
|
+
VERSION = '1.0.2'
|
6
|
+
|
7
|
+
GROWL_IS_READY = "Lend Me Some Sugar; I Am Your Neighbor!"
|
8
|
+
GROWL_NOTIFICATION_CLICKED = "GrowlClicked!"
|
9
|
+
GROWL_NOTIFICATION_TIMED_OUT = "GrowlTimedOut!"
|
10
|
+
GROWL_KEY_CLICKED_CONTEXT = "ClickedContext"
|
11
|
+
|
12
|
+
PRIORITIES = {
|
13
|
+
:emergency => 2,
|
14
|
+
:high => 1,
|
15
|
+
:normal => 0,
|
16
|
+
:moderate => -1,
|
17
|
+
:very_low => -2,
|
18
|
+
}
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Returns the singleton instance of Growl::Notifier with which you register and send your Growl notifications.
|
22
|
+
def sharedInstance
|
23
|
+
@sharedInstance ||= alloc.init
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :application_name, :application_icon, :notifications, :default_notifications
|
28
|
+
attr_accessor :delegate
|
29
|
+
|
30
|
+
# Set to +true+ if you want to receive delegate callback messages,
|
31
|
+
# <tt>growlNotifierClicked_context</tt> & <tt>growlNotifierTimedOut_context</tt>,
|
32
|
+
# without the need to specify a <tt>:click_context</tt>.
|
33
|
+
#
|
34
|
+
# The default is +false+, which means your application won't receive any delegate
|
35
|
+
# callback messages if the <tt>:click_context</tt> is omitted.
|
36
|
+
attr_accessor :always_callback
|
37
|
+
|
38
|
+
# Registers the applications metadata and the notifications, that your application might send, to Growl.
|
39
|
+
# The +default_notifications+ are notifications that will be enabled by default, the regular +notifications+ are
|
40
|
+
# optional and should be enabled by the user in the Growl system preferences.
|
41
|
+
#
|
42
|
+
# Register the applications name and the notifications that will be used.
|
43
|
+
# * +default_notifications+ defaults to the regular +notifications+.
|
44
|
+
# * +application_icon+ defaults to OSX::NSApplication.sharedApplication.applicationIconImage.
|
45
|
+
#
|
46
|
+
# Growl::Notifier.sharedInstance.register 'FoodApp', ['YourHamburgerIsReady', 'OhSomeoneElseAteIt']
|
47
|
+
#
|
48
|
+
# Register the applications name, the notifications plus the default notifications that will be used and the icon that's to be used in the Growl notifications.
|
49
|
+
#
|
50
|
+
# Growl::Notifier.sharedInstance.register 'FoodApp', ['YourHamburgerIsReady', 'OhSomeoneElseAteIt'], ['DefaultNotification], OSX::NSImage.imageNamed('GreasyHamburger')
|
51
|
+
def register(application_name, notifications, default_notifications = nil, application_icon = nil)
|
52
|
+
@application_name, @application_icon = application_name, (application_icon || OSX::NSApplication.sharedApplication.applicationIconImage)
|
53
|
+
@notifications, @default_notifications = notifications, (default_notifications || notifications)
|
54
|
+
@callbacks = {}
|
55
|
+
send_registration!
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sends a Growl notification.
|
59
|
+
#
|
60
|
+
# * +notification_name+ : the name of one of the notifcations that your apllication registered with Growl. See register for more info.
|
61
|
+
# * +title+ : the title that should be used in the Growl notification.
|
62
|
+
# * +description+ : the body of the Grow notification.
|
63
|
+
# * +options+ : specifies a few optional options:
|
64
|
+
# * <tt>:sticky</tt> : indicates if the Grow notification should "stick" to the screen. Defaults to +false+.
|
65
|
+
# * <tt>:priority</tt> : sets the priority level of the Growl notification. Defaults to 0.
|
66
|
+
# * <tt>:click_context</tt> : a string describing the context of the notification. This is send back to the delegate so you can check what kind of notification it was. If omitted, no delegate messages will be send. You can disable this behaviour by setting always_callback to +true+.
|
67
|
+
# * <tt>:icon</tt> : specifies the icon to be used in the Growl notification. Defaults to the registered +application_icon+, see register for more info.
|
68
|
+
#
|
69
|
+
# Simple example:
|
70
|
+
#
|
71
|
+
# name = 'YourHamburgerIsReady'
|
72
|
+
# title = 'Your hamburger is ready for consumption!'
|
73
|
+
# description = 'Please pick it up at isle 4.'
|
74
|
+
#
|
75
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description)
|
76
|
+
#
|
77
|
+
# Example with optional options:
|
78
|
+
#
|
79
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description, :sticky => true, :priority => 1, :icon => OSX::NSImage.imageNamed('SuperBigHamburger'))
|
80
|
+
#
|
81
|
+
# When you pass notify a block, that block will be used as the callback handler if the Growl notification was clicked. Eg:
|
82
|
+
#
|
83
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description, :sticky => true) do
|
84
|
+
# user_clicked_notification_so_do_something!
|
85
|
+
# end
|
86
|
+
def notify(notification_name, title, description, options = {}, &callback)
|
87
|
+
dict = {
|
88
|
+
:ApplicationName => @application_name,
|
89
|
+
:ApplicationPID => pid,
|
90
|
+
:NotificationName => notification_name,
|
91
|
+
:NotificationTitle => title,
|
92
|
+
:NotificationDescription => description,
|
93
|
+
:NotificationPriority => PRIORITIES[options[:priority]] || options[:priority] || 0
|
94
|
+
}
|
95
|
+
dict[:NotificationIcon] = options[:icon].TIFFRepresentation if options[:icon]
|
96
|
+
dict[:NotificationSticky] = 1 if options[:sticky]
|
97
|
+
|
98
|
+
context = {}
|
99
|
+
context[:user_click_context] = options[:click_context] if options[:click_context]
|
100
|
+
if block_given?
|
101
|
+
@callbacks[callback.object_id] = callback
|
102
|
+
context[:callback_object_id] = callback.object_id.to_s
|
103
|
+
end
|
104
|
+
dict[:NotificationClickContext] = context if always_callback || !context.empty?
|
105
|
+
|
106
|
+
notification_center.postNotificationName_object_userInfo_deliverImmediately(:GrowlNotification, nil, dict, true)
|
107
|
+
end
|
108
|
+
|
109
|
+
def onReady(notification)
|
110
|
+
send_registration!
|
111
|
+
end
|
112
|
+
|
113
|
+
def onClicked(notification)
|
114
|
+
user_context = nil
|
115
|
+
if context = notification.userInfo[GROWL_KEY_CLICKED_CONTEXT]
|
116
|
+
user_context = context[:user_click_context]
|
117
|
+
if callback_object_id = context[:callback_object_id]
|
118
|
+
@callbacks.delete(callback_object_id.to_i).call
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
@delegate.growlNotifierClicked_context(self, user_context) if @delegate && @delegate.respond_to?(:growlNotifierClicked_context)
|
123
|
+
end
|
124
|
+
|
125
|
+
def onTimeout(notification)
|
126
|
+
user_context = nil
|
127
|
+
if context = notification.userInfo[GROWL_KEY_CLICKED_CONTEXT]
|
128
|
+
@callbacks.delete(context[:callback_object_id].to_i) if context[:callback_object_id]
|
129
|
+
user_context = context[:user_click_context]
|
130
|
+
end
|
131
|
+
|
132
|
+
@delegate.growlNotifierTimedOut_context(self, user_context) if @delegate && @delegate.respond_to?(:growlNotifierTimedOut_context)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def pid
|
138
|
+
OSX::NSProcessInfo.processInfo.processIdentifier.to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
def notification_center
|
142
|
+
OSX::NSDistributedNotificationCenter.defaultCenter
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_registration!
|
146
|
+
add_observer 'onReady:', GROWL_IS_READY, false
|
147
|
+
add_observer 'onClicked:', GROWL_NOTIFICATION_CLICKED, true
|
148
|
+
add_observer 'onTimeout:', GROWL_NOTIFICATION_TIMED_OUT, true
|
149
|
+
|
150
|
+
dict = {
|
151
|
+
:ApplicationName => @application_name,
|
152
|
+
:ApplicationIcon => application_icon.TIFFRepresentation,
|
153
|
+
:AllNotifications => @notifications,
|
154
|
+
:DefaultNotifications => @default_notifications
|
155
|
+
}
|
156
|
+
|
157
|
+
notification_center.objc_send(
|
158
|
+
:postNotificationName, :GrowlApplicationRegistrationNotification,
|
159
|
+
:object, nil,
|
160
|
+
:userInfo, dict,
|
161
|
+
:deliverImmediately, true
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def add_observer(selector, name, prepend_name_and_pid)
|
166
|
+
name = "#{@application_name}-#{pid}-#{name}" if prepend_name_and_pid
|
167
|
+
notification_center.addObserver_selector_name_object self, selector, name, nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../growl', __FILE__)
|
2
|
+
|
3
|
+
# Defines a few convenience methods that you can use in your class if you include the Growl module.
|
4
|
+
# Eg:
|
5
|
+
#
|
6
|
+
# class FoodReporter < OSX::NSObject
|
7
|
+
# include Growl
|
8
|
+
#
|
9
|
+
# def hamburger_time!
|
10
|
+
# growl 'YourHamburgerIsReady', 'Your hamburger is ready for consumption!', 'Please pick it up at isle 4.', :priority => 1 do
|
11
|
+
# throw_it_away_before_user_reaches_counter!
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
module Growl
|
16
|
+
# Sends a Growl notification. See Growl::Notifier#notify for more info.
|
17
|
+
def growl(name, title, description, options = {}, &callback)
|
18
|
+
Growl::Notifier.sharedInstance.notify name, title, description, options, &callback
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sends a sticky Growl notification. See Growl::Notifier#notify for more info.
|
22
|
+
def sticky_growl(name, title, description, options = {}, &callback)
|
23
|
+
growl name, title, description, options.merge!(:sticky => true), &callback
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'osx/cocoa'
|
2
|
+
OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
|
3
|
+
|
4
|
+
module Rucola
|
5
|
+
class FSEvents
|
6
|
+
class FSEvent
|
7
|
+
attr_reader :fsevents_object
|
8
|
+
attr_reader :id
|
9
|
+
attr_reader :path
|
10
|
+
def initialize(fsevents_object, id, path)
|
11
|
+
@fsevents_object, @id, @path = fsevents_object, id, path
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns an array of the files/dirs in the path that the event occurred in.
|
15
|
+
# The files are sorted by the modification time, the first entry is the last modified file.
|
16
|
+
def files
|
17
|
+
Dir.glob("#{File.expand_path(path)}/*").map do |filename|
|
18
|
+
begin
|
19
|
+
[File.mtime(filename), filename]
|
20
|
+
rescue Errno::ENOENT
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end.compact.sort.reverse.map { |mtime, filename| filename }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the last modified file in the path that the event occurred in.
|
27
|
+
def last_modified_file
|
28
|
+
files.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class StreamError < StandardError; end
|
33
|
+
|
34
|
+
attr_reader :paths
|
35
|
+
attr_reader :stream
|
36
|
+
|
37
|
+
attr_accessor :allocator
|
38
|
+
attr_accessor :context
|
39
|
+
attr_accessor :since
|
40
|
+
attr_accessor :latency
|
41
|
+
attr_accessor :flags
|
42
|
+
|
43
|
+
# Initializes a new FSEvents `watchdog` object and starts watching the directories you specify for events. The
|
44
|
+
# block is used as a handler for events, which are passed as the block's argument. This method is the easiest
|
45
|
+
# way to start watching some directories if you don't care about the details of setting up the event stream.
|
46
|
+
#
|
47
|
+
# Rucola::FSEvents.start_watching('/tmp') do |events|
|
48
|
+
# events.each { |event| log.debug("#{event.files.inspect} were changed.") }
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# Rucola::FSEvents.start_watching('/var/log/system.log', '/var/log/secure.log', :since => last_id, :latency => 5) do
|
52
|
+
# Growl.notify("Something was added to your log files!")
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Note that the method also returns the FSEvents object. This enables you to control the event stream if you want to.
|
56
|
+
#
|
57
|
+
# fsevents = Rucola::FSEvents.start_watching('/Volumes') do |events|
|
58
|
+
# events.each { |event| Growl.notify("Volume changes: #{event.files.to_sentence}") }
|
59
|
+
# end
|
60
|
+
# fsevents.stop
|
61
|
+
def self.start_watching(*params, &block)
|
62
|
+
fsevents = new(*params, &block)
|
63
|
+
fsevents.create_stream
|
64
|
+
fsevents.start
|
65
|
+
fsevents
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates a new FSEvents `watchdog` object. You can specify a list of paths to watch and options to control the
|
69
|
+
# behaviour of the watchdog. The block you pass serves as a callback when an event is generated on one of the
|
70
|
+
# specified paths.
|
71
|
+
#
|
72
|
+
# fsevents = FSEvents.new('/etc/passwd') { Mailer.send_mail("Someone touched the password file!") }
|
73
|
+
# fsevents.create_stream
|
74
|
+
# fsevents.start
|
75
|
+
#
|
76
|
+
# fsevents = FSEvents.new('/home/upload', :since => UploadWatcher.last_event_id) do |events|
|
77
|
+
# events.each do |event|
|
78
|
+
# UploadWatcher.last_event_id = event.id
|
79
|
+
# event.files.each do |file|
|
80
|
+
# UploadWatcher.logfile.append("#{file} was changed")
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# *:since: The service will report events that have happened after the supplied event ID. Never use 0 because that
|
86
|
+
# will cause every fsevent since the "beginning of time" to be reported. Use OSX::KFSEventStreamEventIdSinceNow
|
87
|
+
# if you want to receive events that have happened after this call. (Default: OSX::KFSEventStreamEventIdSinceNow).
|
88
|
+
# You can find the ID's passed with :since in the events passed to your block.
|
89
|
+
# *:latency: Number of seconds to wait until an FSEvent is reported, this allows the service to bundle events. (Default: 0.0)
|
90
|
+
#
|
91
|
+
# Please refer to the Cocoa documentation for the rest of the options.
|
92
|
+
def initialize(*params, &block)
|
93
|
+
raise ArgumentError, 'No callback block was specified.' unless block_given?
|
94
|
+
|
95
|
+
options = params.last.kind_of?(Hash) ? params.pop : {}
|
96
|
+
@paths = params.flatten
|
97
|
+
|
98
|
+
paths.each { |path| raise ArgumentError, "The specified path (#{path}) does not exist." unless File.exist?(path) }
|
99
|
+
|
100
|
+
@allocator = options[:allocator] || OSX::KCFAllocatorDefault
|
101
|
+
@context = options[:context] || nil
|
102
|
+
@since = options[:since] || OSX::KFSEventStreamEventIdSinceNow
|
103
|
+
@latency = options[:latency] || 0.0
|
104
|
+
@flags = options[:flags] || 0
|
105
|
+
@stream = options[:stream] || nil
|
106
|
+
|
107
|
+
@user_callback = block
|
108
|
+
@callback = Proc.new do |stream, client_callback_info, number_of_events, paths_pointer, event_flags, event_ids|
|
109
|
+
paths_pointer.regard_as('*')
|
110
|
+
events = []
|
111
|
+
number_of_events.times {|i| events << Rucola::FSEvents::FSEvent.new(self, event_ids[i], paths_pointer[i]) }
|
112
|
+
@user_callback.call(events)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Create the stream.
|
117
|
+
# Raises a Rucola::FSEvents::StreamError if the stream could not be created.
|
118
|
+
def create_stream
|
119
|
+
@stream = OSX.FSEventStreamCreate(@allocator, @callback, @context, @paths, @since, @latency, @flags)
|
120
|
+
raise(StreamError, 'Unable to create FSEvents stream.') unless @stream
|
121
|
+
OSX.FSEventStreamScheduleWithRunLoop(@stream, OSX.CFRunLoopGetCurrent, OSX::KCFRunLoopDefaultMode)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Start the stream.
|
125
|
+
# Raises a Rucola::FSEvents::StreamError if the stream could not be started.
|
126
|
+
def start
|
127
|
+
raise(StreamError, 'Unable to start FSEvents stream.') unless OSX.FSEventStreamStart(@stream)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Stop the stream.
|
131
|
+
# You can resume it by calling `start` again.
|
132
|
+
def stop
|
133
|
+
OSX.FSEventStreamStop(@stream)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|