Skip to content
This repository has been archived by the owner on Nov 3, 2022. It is now read-only.

Commit

Permalink
proof of concept
Browse files Browse the repository at this point in the history
  • Loading branch information
stereobooster committed Apr 27, 2011
0 parents commit ed6aa4d
Show file tree
Hide file tree
Showing 12 changed files with 481 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.gem
.bundle
.project
.loadpath
Gemfile.lock
pkg/*
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source "http://rubygems.org"

# Specify your gem's dependencies in rb-fchange.gemspec
gemspec
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# rb-fchange

Code not redy. It's just proof of concept
This is a simple wrapper over the Windows Kernel functions for monitoring the specified directory or subtree.

Example

require 'rb-fchange'

notifier = FChange::Notifier.new
notifier.watch("test", :all_events, :recursive) do |event|
p Time.now.utc
end

Signal.trap('INT') do
p "Bye bye...",
notifier.stop
abort("\n")
end

notifier.run
2 changes: 2 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'bundler'
Bundler::GemHelper.install_tasks
14 changes: 14 additions & 0 deletions lib/rb-fchange.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# The root module of the library, which is laid out as so:
#
# * {Notifier} -- The main class, where the notifications are set up
# * {Watcher} -- A watcher for a single file or directory
# * {Event} -- An filesystem event notification
module FChange

require "rb-fchange/native"
require "rb-fchange/native/flags"
require "rb-fchange/notifier"
require "rb-fchange/watcher"
require "rb-fchange/event"

end
29 changes: 29 additions & 0 deletions lib/rb-fchange/event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module FChange
# An event caused by a change on the filesystem.
# Each {Watcher} can fire many events,
# which are passed to that watcher's callback.
class Event
# The {Watcher} that fired this event.
#
# @return [Watcher]
attr_reader :watcher

# Creates an event from a string of binary data.
# Differs from {Event.consume} in that it doesn't modify the string.
#
# @private
# @param watcher [Watcher] The {Watcher} that fired the event
def initialize(watcher)
@watcher = watcher
end

# Calls the callback of the watcher that fired this event,
# passing in the event itself.
#
# @private
def callback!
@watcher.callback!(self)
end

end
end
70 changes: 70 additions & 0 deletions lib/rb-fchange/native.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require 'Win32API'

module FChange
# This module contains the low-level foreign-function.
# It's an implementation detail, and not meant for users to deal with.
#
# @private
module Native

#
INFINITE = 0xFFFFFFFF

#
WAIT_OBJECT_0 = 0x00000000

# HANDLE FindFirstChangeNotification(
# LPCTSTR lpPathName, // directory name
# BOOL bWatchSubtree, // monitoring option
# DWORD dwNotifyFilter // filter conditions
#);
@_k32FindFirstChangeNotification = Win32API.new("kernel32",
"FindFirstChangeNotification", ['P', 'I', 'L'], 'L')

# @return handle
def k32FindFirstChangeNotification(path, recursive, flag)
@_k32FindFirstChangeNotification.call(path, recursive ? 1 : 0, flag)
end

module_function "k32FindFirstChangeNotification"

# BOOL FindNextChangeNotification(
# HANDLE hChangeHandle // handle to change notification
# );
@_k32FindNextChangeNotification = Win32API.new("kernel32",
"FindNextChangeNotification", ['L'], 'I')

def k32FindNextChangeNotification(handle)
@_k32FindNextChangeNotification.call(handle)
end

module_function "k32FindNextChangeNotification"

# DWORD WaitForMultipleObjects(
# DWORD nCount, // number of handles in array
# CONST HANDLE *lpHandles, // object-handle array
# BOOL bWaitAll, // wait option
# DWORD dwMilliseconds // time-out interval
# );
@_k32WaitForMultipleObjects = Win32API.new("kernel32",
"WaitForMultipleObjects", ['L', 'P', 'I', 'L'], 'L')

def k32WaitForMultipleObjects(count, handles, wait_all, time)
@_k32WaitForMultipleObjects.call(count, handles, wait_all, time)
end

module_function "k32WaitForMultipleObjects"

# BOOL FindCloseChangeNotification(
# HANDLE hChangeHandle
# );
@_k32FindCloseChangeNotification = Win32API.new("kernel32",
"FindCloseChangeNotification", ['L'], 'I')

def k32FindCloseChangeNotification(handle)
@_k32FindCloseChangeNotification.call(handle)
end

module_function "k32FindCloseChangeNotification"
end
end
83 changes: 83 additions & 0 deletions lib/rb-fchange/native/flags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module FChange
module Native
# A module containing all the flags to be passed to {Notifier#watch}.
# @see http://msdn.microsoft.com/en-us/library/aa364417(v=VS.85).aspx
#
# @private
module Flags

# Any file name change in the watched directory or subtree causes a change
# notification wait operation to return. Changes include renaming,
# creating, or deleting a file name.
FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001

# Any directory-name change in the watched directory or subtree causes a
# change notification wait operation to return. Changes include creating
# or deleting a directory.
FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002

# Any attribute change in the watched directory or subtree causes a change
# notification wait operation to return.
FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004

# Any file-size change in the watched directory or subtree causes a change
# notification wait operation to return. The operating system detects a
# change in file size only when the file is written to the disk.
# For operating systems that use extensive caching, detection occurs only
# when the cache is sufficiently flushed.
FILE_NOTIFY_CHANGE_SIZE = 0x00000008

# Any change to the last write-time of files in the watched directory or
# subtree causes a change notification wait operation to return.
# The operating system detects a change to the last write-time only when
# the file is written to the disk. For operating systems that use
# extensive caching, detection occurs only when the cache is sufficiently
# flushed
FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010

# Any change to the last access time of files in the watched directory or
# subtree causes a change notification wait operation to return.
FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020

# Any change to the creation time of files in the watched directory or
# subtree causes a change notification wait operation to return.
FILE_NOTIFY_CHANGE_CREATION = 0x00000040

# Any security-descriptor change in the watched directory or subtree
# causes a change notification wait operation to return.
FILE_NOTIFY_CHANGE_SECURITY = 0x00000100

FILE_NOTIFY_CHANGE_ALL_EVENTS = (
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_CREATION |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_LAST_ACCESS |
FILE_NOTIFY_CHANGE_LAST_WRITE |
FILE_NOTIFY_CHANGE_SECURITY |
FILE_NOTIFY_CHANGE_SIZE
)

# Converts a list of flags to the bitmask that the C API expects.
#
# @param flags [Array<Symbol>]
# @return [Fixnum]
def self.to_mask(flags)
flags.map {|flag| const_get("FILE_NOTIFY_CHANGE_#{flag.to_s.upcase}")}.
inject(0) {|mask, flag| mask | flag}
end

# Converts a bitmask from the C API into a list of flags.
#
# @param mask [Fixnum]
# @return [Array<Symbol>]
def self.from_mask(mask)
constants.map {|c| c.to_s}.select do |c|
next false unless c =~ /^FILE_NOTIFY_CHANGE_/
const_get(c) & mask != 0
end.map {|c| c.sub("FILE_NOTIFY_CHANGE_", "").downcase.to_sym} - [:all_events]
end

end
end
end
138 changes: 138 additions & 0 deletions lib/rb-fchange/notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
module FChange
# Notifier wraps a single instance of FChange.
# It's possible to have more than one instance,
# but usually unnecessary.
#
# @example
# # Create the notifier
# notifier = FChange::Notifier.new
#
# # Run this callback whenever the file path/to/foo.txt is read
# notifier.watch("path/to/foo/", :all_events) do
# puts "foo was accessed!"
# end
#
# # Nothing happens until you run the notifier!
# notifier.run
class Notifier
# A hash from {Watcher} ids to the instances themselves.
#
# @private
# @return [{Fixnum => Watcher}]
attr_reader :watchers

attr_reader :dwChangeHandles
attr_reader :lp_dwChangeHandles

# Creates a new {Notifier}.
#
# @return [Notifier]
def initialize
@watchers = {}
@dwChangeHandles = []
@lp_dwChangeHandles = 0
end

# Adds a new {Watcher} to the queue.
def add_watcher(watcher)

@watchers[watcher.id] = watcher

@dwChangeHandles.push watcher.id

# Create storage for event handles (LONG)
@lp_dwChangeHandles = " " * 4 * dwChangeHandles.count

# Pack event handles into newly created storage area
# to be used for Win32 call
@lp_dwChangeHandles = dwChangeHandles.pack("L" * dwChangeHandles.count)

end

# Watches a file or directory for changes,
# calling the callback when there are.
# This is only activated once \{#process} or \{#run} is called.
#
# **Note that by default, this does not recursively watch subdirectories
# of the watched directory**.
# To do so, use the `:recursive` flag.
#
# `:recursive`
# : Recursively watch any subdirectories that are created.
#
# @param path [String] The path to the file or directory
# @param flags [Array<Symbol>] Which events to watch for
# @yield [event] A block that will be called
# whenever one of the specified events occur
# @yieldparam event [Event] The Event object containing information
# about the event that occured
# @return [Watcher] A Watcher set up to watch this path for these events
# @raise [SystemCallError] if the file or directory can't be watched,
# e.g. if the file isn't found, read access is denied,
# or the flags don't contain any events
def watch(path, *flags, &callback)
recursive = flags.include?(:recursive)
flags = flags - [:recursive]
@flags = flags.freeze
Watcher.new(self, path, recursive, *flags, &callback)
end

# Starts the notifier watching for filesystem events.
# Blocks until \{#stop} is called.
#
# @see #process
def run
@stop = false
process until @stop
end

# Stop watching for filesystem events.
# That is, if we're in a \{#run} loop,
# exit out as soon as we finish handling the events.
def stop
@stop = true
end

# Blocks until there are one or more filesystem events
# that this notifier has watchers registered for.
# Once there are events, the appropriate callbacks are called
# and this function returns.
#
# @see #run
def process
read_events.each {|event| event.callback!}
end

# Blocks until there are one or more filesystem events
# that this notifier has watchers registered for.
# Once there are events, returns their {Event} objects.
#
# @private
def read_events

dwWaitStatus = Native.k32WaitForMultipleObjects(@dwChangeHandles.count,
@lp_dwChangeHandles, 0, Native::INFINITE)

events = []

@dwChangeHandles.each_index do |index|
if dwWaitStatus == Native::WAIT_OBJECT_0 + index

ev = Event.new(@watchers[@dwChangeHandles[index]])
events << ev

r = Native.k32FindNextChangeNotification(@dwChangeHandles[index])
if r == 0
raise SystemCallError.new("Failed to watch", r)
end
end
end
events
end

def close

end

end
end
3 changes: 3 additions & 0 deletions lib/rb-fchange/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module FChange
VERSION = "0.0.1"
end
Loading

0 comments on commit ed6aa4d

Please sign in to comment.