rb-fchange 0.0.2

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.
@@ -0,0 +1,31 @@
1
+ # rb-fchange
2
+
3
+ Code not ready. It's just proof of concept
4
+ This is a simple wrapper over the Windows Kernel functions for monitoring the specified directory or subtree.
5
+
6
+ Example
7
+
8
+ ```ruby
9
+ require 'rb-fchange'
10
+
11
+ notifier = FChange::Notifier.new
12
+ notifier.watch("test", :all_events, :recursive) do |event|
13
+ p Time.now.utc
14
+ end
15
+
16
+ Signal.trap('INT') do
17
+ p "Bye bye...",
18
+ notifier.stop
19
+ abort("\n")
20
+ end
21
+
22
+ notifier.run
23
+ ```
24
+
25
+ ## TODO
26
+
27
+ - add latency setting with 0.5 default
28
+ - default flag for events :all_events
29
+ - rework interface (should more look like rb-fsevent)
30
+ - add specs (can use specs from rb-fsevent)
31
+ - publish on rubygems
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,14 @@
1
+ require "rb-fchange/native"
2
+ require "rb-fchange/native/flags"
3
+ require "rb-fchange/notifier"
4
+ require "rb-fchange/watcher"
5
+ require "rb-fchange/event"
6
+
7
+ # The root module of the library, which is laid out as so:
8
+ #
9
+ # * {Notifier} -- The main class, where the notifications are set up
10
+ # * {Watcher} -- A watcher for a single file or directory
11
+ # * {Event} -- An filesystem event notification
12
+ module FChange
13
+
14
+ end
@@ -0,0 +1,29 @@
1
+ module FChange
2
+ # An event caused by a change on the filesystem.
3
+ # Each {Watcher} can fire many events,
4
+ # which are passed to that watcher's callback.
5
+ class Event
6
+ # The {Watcher} that fired this event.
7
+ #
8
+ # @return [Watcher]
9
+ attr_reader :watcher
10
+
11
+ # Creates an event from a string of binary data.
12
+ # Differs from {Event.consume} in that it doesn't modify the string.
13
+ #
14
+ # @private
15
+ # @param watcher [Watcher] The {Watcher} that fired the event
16
+ def initialize(watcher)
17
+ @watcher = watcher
18
+ end
19
+
20
+ # Calls the callback of the watcher that fired this event,
21
+ # passing in the event itself.
22
+ #
23
+ # @private
24
+ def callback!
25
+ @watcher.callback!(self)
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ require 'win32/api'
2
+
3
+ module FChange
4
+ # This module contains the low-level foreign-function.
5
+ # It's an implementation detail, and not meant for users to deal with.
6
+ #
7
+ # @private
8
+ module Native
9
+
10
+ #
11
+ INFINITE = 0xFFFFFFFF
12
+
13
+ #
14
+ WAIT_OBJECT_0 = 0x00000000
15
+
16
+ # HANDLE FindFirstChangeNotification(
17
+ # LPCTSTR lpPathName, // directory name
18
+ # BOOL bWatchSubtree, // monitoring option
19
+ # DWORD dwNotifyFilter // filter conditions
20
+ #);
21
+ @_k32FindFirstChangeNotification =
22
+ Win32::API.new('FindFirstChangeNotification', ['P', 'I', 'L'], 'L')
23
+
24
+ # @return handle
25
+ def k32FindFirstChangeNotification(path, recursive, flag)
26
+ @_k32FindFirstChangeNotification.call(path, recursive ? 1 : 0, flag)
27
+ end
28
+
29
+ module_function "k32FindFirstChangeNotification"
30
+
31
+ # BOOL FindNextChangeNotification(
32
+ # HANDLE hChangeHandle // handle to change notification
33
+ # );
34
+ @_k32FindNextChangeNotification =
35
+ Win32::API.new('FindNextChangeNotification', ['L'], 'I')
36
+
37
+ def k32FindNextChangeNotification(handle)
38
+ @_k32FindNextChangeNotification.call(handle)
39
+ end
40
+
41
+ module_function "k32FindNextChangeNotification"
42
+
43
+ # DWORD WaitForMultipleObjects(
44
+ # DWORD nCount, // number of handles in array
45
+ # CONST HANDLE *lpHandles, // object-handle array
46
+ # BOOL bWaitAll, // wait option
47
+ # DWORD dwMilliseconds // time-out interval
48
+ # );
49
+ @_k32WaitForMultipleObjects =
50
+ Win32::API.new('WaitForMultipleObjects', ['L', 'P', 'I', 'L'], 'L')
51
+
52
+ def k32WaitForMultipleObjects(count, handles, wait_all, time)
53
+ @_k32WaitForMultipleObjects.call(count, handles, wait_all, time)
54
+ end
55
+
56
+ module_function "k32WaitForMultipleObjects"
57
+
58
+ # BOOL FindCloseChangeNotification(
59
+ # HANDLE hChangeHandle
60
+ # );
61
+ @_k32FindCloseChangeNotification =
62
+ Win32::API.new('FindCloseChangeNotification', ['L'], 'I')
63
+
64
+ def k32FindCloseChangeNotification(handle)
65
+ @_k32FindCloseChangeNotification.call(handle)
66
+ end
67
+
68
+ module_function "k32FindCloseChangeNotification"
69
+ end
70
+ end
@@ -0,0 +1,83 @@
1
+ module FChange
2
+ module Native
3
+ # A module containing all the flags to be passed to {Notifier#watch}.
4
+ # @see http://msdn.microsoft.com/en-us/library/aa364417(v=VS.85).aspx
5
+ #
6
+ # @private
7
+ module Flags
8
+
9
+ # Any file name change in the watched directory or subtree causes a change
10
+ # notification wait operation to return. Changes include renaming,
11
+ # creating, or deleting a file name.
12
+ FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001
13
+
14
+ # Any directory-name change in the watched directory or subtree causes a
15
+ # change notification wait operation to return. Changes include creating
16
+ # or deleting a directory.
17
+ FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002
18
+
19
+ # Any attribute change in the watched directory or subtree causes a change
20
+ # notification wait operation to return.
21
+ FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004
22
+
23
+ # Any file-size change in the watched directory or subtree causes a change
24
+ # notification wait operation to return. The operating system detects a
25
+ # change in file size only when the file is written to the disk.
26
+ # For operating systems that use extensive caching, detection occurs only
27
+ # when the cache is sufficiently flushed.
28
+ FILE_NOTIFY_CHANGE_SIZE = 0x00000008
29
+
30
+ # Any change to the last write-time of files in the watched directory or
31
+ # subtree causes a change notification wait operation to return.
32
+ # The operating system detects a change to the last write-time only when
33
+ # the file is written to the disk. For operating systems that use
34
+ # extensive caching, detection occurs only when the cache is sufficiently
35
+ # flushed
36
+ FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010
37
+
38
+ # Any change to the last access time of files in the watched directory or
39
+ # subtree causes a change notification wait operation to return.
40
+ FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020
41
+
42
+ # Any change to the creation time of files in the watched directory or
43
+ # subtree causes a change notification wait operation to return.
44
+ FILE_NOTIFY_CHANGE_CREATION = 0x00000040
45
+
46
+ # Any security-descriptor change in the watched directory or subtree
47
+ # causes a change notification wait operation to return.
48
+ FILE_NOTIFY_CHANGE_SECURITY = 0x00000100
49
+
50
+ FILE_NOTIFY_CHANGE_ALL_EVENTS = (
51
+ FILE_NOTIFY_CHANGE_ATTRIBUTES |
52
+ FILE_NOTIFY_CHANGE_CREATION |
53
+ FILE_NOTIFY_CHANGE_DIR_NAME |
54
+ FILE_NOTIFY_CHANGE_FILE_NAME |
55
+ FILE_NOTIFY_CHANGE_LAST_ACCESS |
56
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
57
+ FILE_NOTIFY_CHANGE_SECURITY |
58
+ FILE_NOTIFY_CHANGE_SIZE
59
+ )
60
+
61
+ # Converts a list of flags to the bitmask that the C API expects.
62
+ #
63
+ # @param flags [Array<Symbol>]
64
+ # @return [Fixnum]
65
+ def self.to_mask(flags)
66
+ flags.map {|flag| const_get("FILE_NOTIFY_CHANGE_#{flag.to_s.upcase}")}.
67
+ inject(0) {|mask, flag| mask | flag}
68
+ end
69
+
70
+ # Converts a bitmask from the C API into a list of flags.
71
+ #
72
+ # @param mask [Fixnum]
73
+ # @return [Array<Symbol>]
74
+ def self.from_mask(mask)
75
+ constants.map {|c| c.to_s}.select do |c|
76
+ next false unless c =~ /^FILE_NOTIFY_CHANGE_/
77
+ const_get(c) & mask != 0
78
+ end.map {|c| c.sub("FILE_NOTIFY_CHANGE_", "").downcase.to_sym} - [:all_events]
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,139 @@
1
+ module FChange
2
+ # Notifier wraps a single instance of FChange.
3
+ # It's possible to have more than one instance,
4
+ # but usually unnecessary.
5
+ #
6
+ # @example
7
+ # # Create the notifier
8
+ # notifier = FChange::Notifier.new
9
+ #
10
+ # # Run this callback whenever the file path/to/foo.txt is read
11
+ # notifier.watch("path/to/foo/", :all_events) do
12
+ # puts "foo was accessed!"
13
+ # end
14
+ #
15
+ # # Nothing happens until you run the notifier!
16
+ # notifier.run
17
+ class Notifier
18
+ # A hash from {Watcher} ids to the instances themselves.
19
+ #
20
+ # @private
21
+ # @return [{Fixnum => Watcher}]
22
+ attr_reader :watchers
23
+
24
+ attr_reader :dwChangeHandles
25
+ attr_reader :lp_dwChangeHandles
26
+
27
+ # Creates a new {Notifier}.
28
+ #
29
+ # @return [Notifier]
30
+ def initialize
31
+ @watchers = {}
32
+ @dwChangeHandles = []
33
+ @lp_dwChangeHandles = 0
34
+ end
35
+
36
+ # Adds a new {Watcher} to the queue.
37
+ def add_watcher(watcher)
38
+
39
+ @watchers[watcher.id] = watcher
40
+
41
+ @dwChangeHandles.push watcher.id
42
+
43
+ # Pack event handles into newly created storage area
44
+ # to be used for Win32 call
45
+ @lp_dwChangeHandles = dwChangeHandles.pack("L" * dwChangeHandles.count)
46
+
47
+ end
48
+
49
+ # Watches a file or directory for changes,
50
+ # calling the callback when there are.
51
+ # This is only activated once \{#process} or \{#run} is called.
52
+ #
53
+ # **Note that by default, this does not recursively watch subdirectories
54
+ # of the watched directory**.
55
+ # To do so, use the `:recursive` flag.
56
+ #
57
+ # `:recursive`
58
+ # : Recursively watch any subdirectories that are created.
59
+ #
60
+ # @param path [String] The path to the file or directory
61
+ # @param flags [Array<Symbol>] Which events to watch for
62
+ # @yield [event] A block that will be called
63
+ # whenever one of the specified events occur
64
+ # @yieldparam event [Event] The Event object containing information
65
+ # about the event that occured
66
+ # @return [Watcher] A Watcher set up to watch this path for these events
67
+ # @raise [SystemCallError] if the file or directory can't be watched,
68
+ # e.g. if the file isn't found, read access is denied,
69
+ # or the flags don't contain any events
70
+ def watch(path, *flags, &callback)
71
+ recursive = flags.include?(:recursive)
72
+ flags.include?(:recursive)
73
+ #:latency = 0.5
74
+ flags = flags - [:recursive]
75
+ @flags = flags.freeze
76
+ Watcher.new(self, path, recursive, *flags, &callback)
77
+ end
78
+
79
+ # Starts the notifier watching for filesystem events.
80
+ # Blocks until \{#stop} is called.
81
+ #
82
+ # @see #process
83
+ def run
84
+ @stop = false
85
+ process until @stop
86
+ end
87
+
88
+ # Stop watching for filesystem events.
89
+ # That is, if we're in a \{#run} loop,
90
+ # exit out as soon as we finish handling the events.
91
+ def stop
92
+ @stop = true
93
+ end
94
+
95
+ # Blocks until there are one or more filesystem events
96
+ # that this notifier has watchers registered for.
97
+ # Once there are events, the appropriate callbacks are called
98
+ # and this function returns.
99
+ #
100
+ # @see #run
101
+ def process
102
+ read_events.each {|event| event.callback!}
103
+ end
104
+
105
+ # Blocks until there are one or more filesystem events
106
+ # that this notifier has watchers registered for.
107
+ # Once there are events, returns their {Event} objects.
108
+ #
109
+ # @private
110
+ def read_events
111
+
112
+ # can return WAIT_TIMEOUT = 0x00000102
113
+ dwWaitStatus = Native.k32WaitForMultipleObjects(@dwChangeHandles.count,
114
+ @lp_dwChangeHandles, 0, 500)
115
+
116
+ events = []
117
+
118
+ # this call blocks all threads completely.
119
+ @dwChangeHandles.each_index do |index|
120
+ if dwWaitStatus == Native::WAIT_OBJECT_0 + index
121
+
122
+ ev = Event.new(@watchers[@dwChangeHandles[index]])
123
+ events << ev
124
+
125
+ r = Native.k32FindNextChangeNotification(@dwChangeHandles[index])
126
+ if r == 0
127
+ raise SystemCallError.new("Failed to watch", r)
128
+ end
129
+ end
130
+ end
131
+ events
132
+ end
133
+
134
+ def close
135
+
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,80 @@
1
+ module FChange
2
+ # Watchers monitor a single path for changes,
3
+ # specified by {FChange::Notifier#watch event flags}.
4
+ # A watcher is usually created via \{Notifier#watch}.
5
+ #
6
+ # One {Notifier} may have many {Watcher}s.
7
+ # The Notifier actually takes care of the checking for events,
8
+ # via \{Notifier#run #run} or \{Notifier#process #process}.
9
+ # The main purpose of having Watcher objects
10
+ # is to be able to disable them using \{#close}.
11
+ class Watcher
12
+ # The {Notifier} that this Watcher belongs to.
13
+ #
14
+ # @return [Notifier]
15
+ attr_reader :notifier
16
+
17
+ # The path that this Watcher is watching.
18
+ #
19
+ # @return [String]
20
+ attr_reader :path
21
+
22
+ # The {FChange::Notifier#watch flags}
23
+ # specifying the events that this Watcher is watching for,
24
+ # and potentially some options as well.
25
+ #
26
+ # @return [Array<Symbol>]
27
+ attr_reader :flags
28
+
29
+ # The id for this Watcher.
30
+ # Used to retrieve this Watcher from {Notifier#watchers}.
31
+ #
32
+ # @private
33
+ # @return [Fixnum]
34
+ attr_reader :id
35
+
36
+ #
37
+ # @private
38
+ # @return [Boolean]
39
+ attr_reader :recursive
40
+
41
+ # Calls this Watcher's callback with the given {Event}.
42
+ #
43
+ # @private
44
+ # @param event [Event]
45
+ def callback!(event)
46
+ @callback[event]
47
+ end
48
+
49
+ # Disables this Watcher, so that it doesn't fire any more events.
50
+ #
51
+ # @raise [SystemCallError] if the watch fails to be disabled for some reason
52
+ def close
53
+ r = Native.k32FindCloseChangeNotification(@id)
54
+ #@notifier.remove_watcher(self)
55
+ return if r == 0
56
+ raise SystemCallError.new("Failed to stop watching #{@path.inspect}", r)
57
+ end
58
+
59
+ # Creates a new {Watcher}.
60
+ #
61
+ # @private
62
+ # @see Notifier#watch
63
+ def initialize(notifier, path, recursive, *flags, &callback)
64
+ @notifier = notifier
65
+ @callback = callback || proc {}
66
+ @path = path
67
+ @flags = flags
68
+ @recursive = recursive
69
+ @id = Native.k32FindFirstChangeNotification(path, recursive,
70
+ Native::Flags.to_mask(flags));
71
+
72
+ unless @id < 0
73
+ @notifier.add_watcher(self)
74
+ return
75
+ end
76
+
77
+ raise SystemCallError.new("Failed to watch #{path.inspect}", @id)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rb-fchange/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{rb-fchange}
7
+ s.version = FChange::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["stereobooster"]
10
+ s.date = %q{2011-04-28}
11
+ s.description = %q{A Ruby wrapper for Windows Kernel functions for monitoring the specified directory or subtree}
12
+ s.email = %q{stereobooster@gmail.com}
13
+ s.extra_rdoc_files = [
14
+ "README.md"
15
+ ]
16
+ s.files = [
17
+ "README.md",
18
+ "Rakefile",
19
+ "lib/rb-fchange.rb",
20
+ "lib/rb-fchange/event.rb",
21
+ "lib/rb-fchange/native.rb",
22
+ "lib/rb-fchange/native/flags.rb",
23
+ "lib/rb-fchange/notifier.rb",
24
+ "lib/rb-fchange/watcher.rb",
25
+ "rb-fchange.gemspec"
26
+ ]
27
+ s.homepage = %q{http://github.com/stereobooster/rb-fchange}
28
+ s.require_paths = ["lib"]
29
+ s.rubygems_version = %q{1.3.7}
30
+ s.summary = %q{A Ruby wrapper for Windows Kernel functions for monitoring the specified directory or subtree}
31
+ s.add_dependency('win32-api', '>= 1.4.8')
32
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rb-fchange
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - stereobooster
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-28 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: win32-api
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 1
32
+ - 4
33
+ - 8
34
+ version: 1.4.8
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: A Ruby wrapper for Windows Kernel functions for monitoring the specified directory or subtree
38
+ email: stereobooster@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.md
45
+ files:
46
+ - README.md
47
+ - Rakefile
48
+ - lib/rb-fchange.rb
49
+ - lib/rb-fchange/event.rb
50
+ - lib/rb-fchange/native.rb
51
+ - lib/rb-fchange/native/flags.rb
52
+ - lib/rb-fchange/notifier.rb
53
+ - lib/rb-fchange/watcher.rb
54
+ - rb-fchange.gemspec
55
+ has_rdoc: true
56
+ homepage: http://github.com/stereobooster/rb-fchange
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options: []
61
+
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ hash: 3
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.5.2
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: A Ruby wrapper for Windows Kernel functions for monitoring the specified directory or subtree
89
+ test_files: []
90
+