aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/beer_events.rb
blob: 18d0d0a6cf608abb25bd04f4eeaad4f4c1fd4478 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# Vendored from Rucola: http://github.com/alloy/rucola/tree/master
#
# Copyright (c) 2007, 2008, 2009 Eloy Duran <eloy.de.enige@gmail.com>
#
# 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.

begin
  require 'osx/cocoa'
  OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
  
  module Rucola
    class FSEvents
      class FSEvent
        attr_reader :fsevents_object
        attr_reader :id
        attr_reader :path
        def initialize(fsevents_object, id, path)
          @fsevents_object, @id, @path = fsevents_object, id, path
        end
        
        # Returns an array of the files/dirs in the path that the event occurred in.
        # The files are sorted by the modification time, the first entry is the last modified file.
        def files
          Dir.glob("#{File.expand_path(path)}/*").sort_by {|f| File.mtime(f) }.reverse
        end
        
        # Returns the last modified file in the path that the event occurred in.
        def last_modified_file
          files.first
        end
      end
      
      class StreamError < StandardError; end
      
      attr_reader :paths
      attr_reader :stream
      
      attr_accessor :allocator
      attr_accessor :context
      attr_accessor :since
      attr_accessor :latency
      attr_accessor :flags
      
      # Initializes a new FSEvents `watchdog` object and starts watching the directories you specify for events. The
      # block is used as a handler for events, which are passed as the block's argument. This method is the easiest
      # way to start watching some directories if you don't care about the details of setting up the event stream.
      #
      #   Rucola::FSEvents.start_watching('/tmp') do |events|
      #     events.each { |event| log.debug("#{event.files.inspect} were changed.") }
      #   end
      #   
      #   Rucola::FSEvents.start_watching('/var/log/system.log', '/var/log/secure.log', :since => last_id, :latency => 5) do
      #     Growl.notify("Something was added to your log files!")
      #   end
      #
      # Note that the method also returns the FSEvents object. This enables you to control the event stream if you want to.
      #
      #   fsevents = Rucola::FSEvents.start_watching('/Volumes') do |events|
      #     events.each { |event| Growl.notify("Volume changes: #{event.files.to_sentence}") }
      #   end
      #   fsevents.stop
      def self.start_watching(*params, &block)
        fsevents = new(*params, &block)
        fsevents.create_stream
        fsevents.start
        fsevents
      end
      
      # Creates a new FSEvents `watchdog` object. You can specify a list of paths to watch and options to control the
      # behaviour of the watchdog. The block you pass serves as a callback when an event is generated on one of the
      # specified paths.
      #
      #   fsevents = FSEvents.new('/etc/passwd') { Mailer.send_mail("Someone touched the password file!") }
      #   fsevents.create_stream
      #   fsevents.start
      #
      #   fsevents = FSEvents.new('/home/upload', :since => UploadWatcher.last_event_id) do |events|
      #     events.each do |event|
      #       UploadWatcher.last_event_id = event.id
      #       event.files.each do |file|
      #         UploadWatcher.logfile.append("#{file} was changed")
      #       end
      #     end
      #   end
      #
      # *:since: The service will report events that have happened after the supplied event ID. Never use 0 because that 
      #   will cause every fsevent since the "beginning of time" to be reported. Use OSX::KFSEventStreamEventIdSinceNow
      #   if you want to receive events that have happened after this call. (Default: OSX::KFSEventStreamEventIdSinceNow).
      #   You can find the ID's passed with :since in the events passed to your block.
      # *:latency: Number of seconds to wait until an FSEvent is reported, this allows the service to bundle events. (Default: 0.0)
      #
      # Please refer to the Cocoa documentation for the rest of the options.
      def initialize(*params, &block)
        raise ArgumentError, 'No callback block was specified.' unless block_given?
        
        options = params.last.kind_of?(Hash) ? params.pop : {}
        @paths = params.flatten
        
        paths.each { |path| raise ArgumentError, "The specified path (#{path}) does not exist." unless File.exist?(path) }
        
        @allocator = options[:allocator] || OSX::KCFAllocatorDefault
        @context   = options[:context]   || nil
        @since     = options[:since]     || OSX::KFSEventStreamEventIdSinceNow
        @latency   = options[:latency]   || 0.0
        @flags     = options[:flags]     || 0
        @stream    = options[:stream]    || nil
        
        @user_callback = block
        @callback = Proc.new do |stream, client_callback_info, number_of_events, paths_pointer, event_flags, event_ids|
          paths_pointer.regard_as('*')
          events = []
          number_of_events.times {|i| events << Rucola::FSEvents::FSEvent.new(self, event_ids[i], paths_pointer[i]) }
          @user_callback.call(events)
        end
      end
      
      # Create the stream.
      # Raises a Rucola::FSEvents::StreamError if the stream could not be created.
      def create_stream
        @stream = OSX.FSEventStreamCreate(@allocator, @callback, @context, @paths, @since, @latency, @flags)
        raise(StreamError, 'Unable to create FSEvents stream.') unless @stream
        OSX.FSEventStreamScheduleWithRunLoop(@stream, OSX.CFRunLoopGetCurrent, OSX::KCFRunLoopDefaultMode)
      end
      
      # Start the stream.
      # Raises a Rucola::FSEvents::StreamError if the stream could not be started.
      def start
        raise(StreamError, 'Unable to start FSEvents stream.') unless OSX.FSEventStreamStart(@stream)
      end
      
      # Stop the stream.
      # You can resume it by calling `start` again.
      def stop
        OSX.FSEventStreamStop(@stream)
      end
    end
  end
  
  # The complete BeerEvents API :)
  HOMEBREW_KEEP_DRY = %w{ /System /usr /etc /sbin /bin /Applications /Library }
  
  def watch_out_for_spill
    # Disable the RubyCocoa thread hook as apparently Laurent did not apply the
    # thread patches to the OS X system Ruby
    ENV['RUBYCOCOA_THREAD_HOOK_DISABLE'] = 'kampai'
    
    Thread.new { OSX.CFRunLoopRun() }
    
    start = Time.now
    dog = Rucola::FSEvents.start_watching(*HOMEBREW_KEEP_DRY) do |events|
      spill = events.map { |e| e.files }.flatten
      spill.reject! { |f| File.mtime(f) < start }
      spill.reject! { |path| path =~ /^#{HOMEBREW_PREFIX}/ }
      # irrelevent files that change a lot
      spill.reject! { |path| path == "/Library/Preferences/SystemConfiguration/com.apple.PowerManagement.plist-lock" }
      spill.reject! { |path| path =~ %r{^(/System)?/Library/Caches/} }
      unless spill.empty?
        opoo "Detected installation of files outside the Homebrew prefix:"
        puts *spill
      end
    end
    yield
  ensure
    dog.stop
  end
rescue LoadError => e
  onoe "RubyCocoa could not be loaded, therefore checking for spill is disabled."
  puts "When using a custom Ruby installation, you'll need to install RubyCocoa."
  puts "If this is not the case, see if the following ticket applies, or create one."
  puts "  http://github.com/mxcl/homebrew/issues#issue/37"
  
  if ARGV.verbose?
    onoe e.message
    puts e.backtrace
  end
  
  def watch_out_for_spill
    yield
  end
end