Skip to content

Commit

Permalink
watcher-rb: first draft of ruby support
Browse files Browse the repository at this point in the history
  • Loading branch information
Will committed Jul 21, 2024
1 parent a62beba commit 4030e97
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 0 deletions.
2 changes: 2 additions & 0 deletions watcher-rb/.bundle/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
BUNDLE_PATH: "vendor/bundle"
6 changes: 6 additions & 0 deletions watcher-rb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
vendor
tmp
pkg
*.gem
ext/watcher/watcher.cpp
ext/watcher/wtr
10 changes: 10 additions & 0 deletions watcher-rb/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

gem 'rake'
gem 'rspec'
gem 'solargraph'
gem 'standard'
112 changes: 112 additions & 0 deletions watcher-rb/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
PATH
remote: .
specs:
watcher (0.11.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
backport (1.2.0)
benchmark (0.3.0)
diff-lcs (1.5.1)
e2mmap (0.1.0)
jaro_winkler (1.6.0)
json (2.7.2)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
nokogiri (1.16.6-arm64-darwin)
racc (~> 1.4)
parallel (1.24.0)
parser (3.3.4.0)
ast (~> 2.4.1)
racc
racc (1.8.0)
rainbow (3.1.1)
rake (13.2.1)
rbs (2.8.4)
regexp_parser (2.9.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.3.2)
strscan
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-performance (1.16.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
ruby-progressbar (1.13.0)
solargraph (0.50.0)
backport (~> 1.2)
benchmark
bundler (~> 2.0)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
parser (~> 3.0)
rbs (~> 2.0)
reverse_markdown (~> 2.0)
rubocop (~> 1.38)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
standard (1.28.5)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50.2)
standard-custom (~> 1.0.0)
standard-performance (~> 1.0.1)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.0.1)
lint_roller (~> 1.0)
rubocop-performance (~> 1.16.0)
strscan (3.1.0)
thor (1.3.1)
tilt (2.4.0)
unicode-display_width (2.5.0)
yard (0.9.36)

PLATFORMS
arm64-darwin-23

DEPENDENCIES
bundler (~> 2.0)
rake
rspec
solargraph
standard
watcher!

BUNDLED WITH
2.3.26
20 changes: 20 additions & 0 deletions watcher-rb/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task build_shared_library: ["lib"] do
framework_deps = case RbConfig::CONFIG["host_os"]
when /darwin/
"-framework CoreServices -framework CoreFoundation"
else
""
end
version = File.read("../.version").strip
libname = "libwatcher-c-#{version}.so"
sh "c++ -shared -std=c++17 -O2 #{framework_deps} ../watcher-c/src/watcher-c.cpp -I ../watcher-c/include -o lib/#{libname}"
end

task default: [:clobber, :build_shared_library, :spec]
15 changes: 15 additions & 0 deletions watcher-rb/bin/watcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "watcher"

if ARGV.empty?
puts("Usage: watcher <path>")
exit(1)
end

path = ARGV[0]
watcher = Watcher::Watch.new(path, method(:puts))
ObjectSpace.define_finalizer(watcher, Watcher::Watch.method(:finalize).to_proc)
puts("Watching #{path}. Press Enter to stop.")
gets
Binary file added watcher-rb/lib/libwatcher-c-0.11.0.so
Binary file not shown.
192 changes: 192 additions & 0 deletions watcher-rb/lib/watcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# frozen_string_literal: true

require 'fiddle'
require 'date'

module Watcher
class CEvent < Fiddle::Struct
layout(
:path_name,
:pointer,
:effect_type,
:int8_t,
:path_type,
:int8_t,
:effect_time,
:int64_t,
:associated_path_name,
:pointer
)
end

LIB = nil

def native_solib_file_ending
case RbConfig::CONFIG['host_os']
when /darwin/
'so'
when /mswin|mingw|cygwin/
'dll'
else
'so'
end
end

def libwatcher_c_lib_path
version = '0.11.0' # hook: tool/release
lib_name = "libwatcher-c-#{version}.#{native_solib_file_ending}"
lib_path = File.join(File.dirname(__FILE__), lib_name)
raise "Library does not exist: '#{lib_path}'" unless File.exist?(lib_path)

puts("Using library: '#{lib_path}'")
lib_path
end

def self.lazy_static_solib_handle
return LIB if LIB

@lib = Fiddle.dlopen(libwatcher_c_lib_path)
@lib.extern('void* wtr_watcher_open(char*, void*, void*)')
@lib.extern('bool wtr_watcher_close(void*)')
@lib
end

def to_utf8
return '' if @value.nil?
return @value if @value.is_a?(String)
return @value.to_s.force_encoding('UTF-8') if @value.respond_to?(:to_s)

raise TypeError
end

def self.c_event_to_event(c_event)
path_name = as_utf8(c_event.path_name.to_s)
associated_path_name = as_utf8(c_event.associated_path_name.to_s)
effect_type = EffectType.new(c_event.effect_type)
path_type = PathType.new(c_event.path_type)
effect_time = Time.at(c_event.effect_time / 1e9)
Event.new(path_name, effect_type, path_type, effect_time, associated_path_name)
end

class EffectType
RENAME = 0
MODIFY = 1
CREATE = 2
DESTROY = 3
OWNER = 4
OTHER = 5

def initialize(value)
@value = value
end

def to_s
case @value
when RENAME
'RENAME'
when MODIFY
'MODIFY'
when CREATE
'CREATE'
when DESTROY
'DESTROY'
when OWNER
'OWNER'
when OTHER
'OTHER'
else
'UNKNOWN'
end
end
end

class PathType
DIR = 0
FILE = 1
HARD_LINK = 2
SYM_LINK = 3
WATCHER = 4
OTHER = 5

def initialize(value)
@value = value
end

def to_s
case @value
when DIR
'DIR'
when FILE
'FILE'
when HARD_LINK
'HARD_LINK'
when SYM_LINK
'SYM_LINK'
when WATCHER
'WATCHER'
when OTHER
'OTHER'
else
'UNKNOWN'
end
end
end

class Event
attr_reader :path_name, :effect_type, :path_type, :effect_time, :associated_path_name

def initialize(path_name, effect_type, path_type, effect_time, associated_path_name)
@path_name = path_name
@effect_type = effect_type
@path_type = path_type
@effect_time = effect_time
@associated_path_name = associated_path_name
end

def to_s
pnm = "path_name: #{@path_name}"
ety = "effect_type: #{@effect_type}"
pty = "path_type: #{@path_type}"
etm = "effect_time: #{@effect_time}"
apn = "associated_path_name: #{@associated_path_name}"
"Event(#{pnm}, #{ety}, #{pty}, #{etm}, #{apn})"
end
end

class Watch
def initialize(path, callback)
@lib = Watcher.lazy_static_solib_handle
@path = path.encode('UTF-8')
@callback = callback
@c_callback = Fiddle::Closure::BlockCaller.new(0, [CEvent, :void]) do |c_event, _|
py_event = Watcher.c_event_to_event(c_event)
@callback.call(py_event)
end

@watcher = @lib.wtr_watcher_open(@path, @c_callback, nil)
raise 'Failed to open a watcher' unless @watcher
end

def close
return unless @watcher

@lib.wtr_watcher_close(@watcher)
@watcher = nil
end

def finalize
close
end

def self.finalize(id)
ObjectSpace._id2ref(id).close
end
end
end

if __FILE__ == $PROGRAM_NAME
events_at = ARGV[0] || '.'
watcher = Watcher::Watch.new(events_at, method(:puts))
ObjectSpace.define_finalizer(watcher, Watcher::Watch.method(:finalize).to_proc)
gets
end
7 changes: 7 additions & 0 deletions watcher-rb/tool/build
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#! /usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
bundle install
bundle exec rake
bundle exec gem build watcher.gemspec
bundle exec gem install watcher-*.gem
23 changes: 23 additions & 0 deletions watcher-rb/watcher.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

Gem::Specification.new do |spec|
spec.name = 'watcher'
spec.version = File.read('../.version').strip
spec.authors = ['Will']
spec.email = ['[email protected]']
spec.summary = 'Filesystem watcher. Simple, efficient and friendly.'
spec.description = 'Watch events on (almost) any filesystem.'
spec.homepage = 'https://github.com/e-dant/watcher'
spec.license = 'MIT'
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['changelog_uri'] = "#{spec.homepage}/changelog.md"
spec.metadata['allowed_push_host'] = 'TODO'
spec.files = `ls -1 lib`.split("\n").map { |f| "lib/#{f}" }
spec.bindir = 'bin'
spec.executables = 'watcher'
spec.require_paths = ['lib']
spec.add_development_dependency 'bundler', '~> 2.0'
spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'rspec', '~> 3.0'
end

0 comments on commit 4030e97

Please sign in to comment.