watchcat 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,228 @@
1
+ use crossbeam_channel::{select, unbounded};
2
+ use magnus::{
3
+ block::{block_given, yield_value},
4
+ class::object,
5
+ define_module, function, method,
6
+ scan_args::{get_kwargs, scan_args},
7
+ Error, Module, Object, Value,
8
+ };
9
+ use notify::{Config, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher};
10
+ use notify_debouncer_mini::new_debouncer;
11
+ use std::{path::Path, time::Duration};
12
+
13
+ mod event;
14
+ use crate::event::WatchatEvent;
15
+
16
+ #[magnus::wrap(class = "Watchcat::Watcher")]
17
+ struct WatchcatWatcher {
18
+ tx: crossbeam_channel::Sender<bool>,
19
+ rx: crossbeam_channel::Receiver<bool>,
20
+ }
21
+
22
+ #[derive(Debug)]
23
+ enum WatcherEnum {
24
+ #[allow(dead_code)]
25
+ Poll(PollWatcher),
26
+ #[allow(dead_code)]
27
+ Recommended(RecommendedWatcher),
28
+ }
29
+
30
+ impl WatchcatWatcher {
31
+ fn new() -> Self {
32
+ let (tx_executor, rx_executor) = unbounded::<bool>();
33
+ Self {
34
+ tx: tx_executor,
35
+ rx: rx_executor,
36
+ }
37
+ }
38
+
39
+ fn close(&self) {
40
+ self.tx.send(true).unwrap()
41
+ }
42
+
43
+ fn watch(&self, args: &[Value]) -> Result<bool, Error> {
44
+ if !block_given() {
45
+ return Err(Error::new(magnus::exception::arg_error(), "no block given"));
46
+ }
47
+
48
+ let (pathnames, recursive, force_polling, poll_interval, ignore_remove, debounce) = Self::parse_args(args)?;
49
+ let mode = if recursive {
50
+ RecursiveMode::Recursive
51
+ } else {
52
+ RecursiveMode::NonRecursive
53
+ };
54
+
55
+ if debounce >= 0 {
56
+ self.watch_with_debounce(pathnames, mode, ignore_remove, debounce)
57
+ } else {
58
+ self.watch_without_debounce(pathnames, mode, force_polling, poll_interval, ignore_remove)
59
+ }
60
+ }
61
+
62
+ fn watch_without_debounce(&self, pathnames: Vec<String>, mode: RecursiveMode, force_polling: bool, poll_interval: u64, ignore_remove: bool) -> Result<bool, Error> {
63
+ let (tx, rx) = unbounded();
64
+ // This variable is needed to keep `watcher` active.
65
+ let _watcher = match force_polling {
66
+ true => {
67
+ let delay = Duration::from_millis(poll_interval);
68
+ let config = notify::Config::default().with_poll_interval(delay);
69
+ let mut watcher = PollWatcher::new(tx, config)
70
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
71
+ for pathname in &pathnames {
72
+ let path = Path::new(pathname);
73
+ watcher
74
+ .watch(path, mode)
75
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
76
+ }
77
+ WatcherEnum::Poll(watcher)
78
+ }
79
+ false => {
80
+ let mut watcher = RecommendedWatcher::new(tx, Config::default())
81
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
82
+ for pathname in &pathnames {
83
+ let path = Path::new(pathname);
84
+ watcher
85
+ .watch(path, mode)
86
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
87
+ }
88
+ WatcherEnum::Recommended(watcher)
89
+ }
90
+ };
91
+
92
+ loop {
93
+ select! {
94
+ recv(self.rx) -> _res => {
95
+ return Ok(true)
96
+ }
97
+ recv(rx) -> res => {
98
+ match res {
99
+ Ok(event) => {
100
+ match event {
101
+ Ok(event) => {
102
+ let paths = event
103
+ .paths
104
+ .iter()
105
+ .map(|p| p.to_string_lossy().into_owned())
106
+ .collect::<Vec<_>>();
107
+
108
+ if ignore_remove && matches!(event.kind, notify::event::EventKind::Remove(_)) {
109
+ continue;
110
+ }
111
+
112
+ yield_value::<(Vec<String>, Vec<String>, String), Value>(
113
+
114
+ (WatchatEvent::convert_kind(&event.kind), paths, format!("{:?}", event.kind))
115
+ )?;
116
+ }
117
+ Err(e) => {
118
+ return Err(
119
+ Error::new(magnus::exception::runtime_error(), e.to_string())
120
+ )
121
+ }
122
+ }
123
+ }
124
+ Err(e) => {
125
+ return Err(
126
+ Error::new(magnus::exception::runtime_error(), e.to_string())
127
+ )
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ fn watch_with_debounce(&self, pathnames: Vec<String>, mode: RecursiveMode, ignore_remove: bool, debounce: i64) -> Result<bool, Error> {
136
+ let (tx, rx) = unbounded();
137
+ let mut debouncer = new_debouncer(Duration::from_millis(debounce.try_into().unwrap()), tx).unwrap();
138
+ for pathname in &pathnames {
139
+ let path = Path::new(pathname);
140
+ debouncer
141
+ .watcher()
142
+ .watch(path, mode)
143
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
144
+ }
145
+
146
+ loop {
147
+ select! {
148
+ recv(self.rx) -> _res => {
149
+ return Ok(true)
150
+ }
151
+ recv(rx) -> res => {
152
+ match res {
153
+ Ok(events) => {
154
+ match events {
155
+ Ok(events) => {
156
+ events.iter().for_each(|event| {
157
+ if ignore_remove && !Path::new(&event.path).exists() {
158
+ return;
159
+ }
160
+
161
+ yield_value::<(Vec<String>, Vec<String>, String), Value>(
162
+ (vec![], vec![event.path.to_string_lossy().into_owned()], format!("{:?}", event.kind))
163
+ ).unwrap();
164
+ });
165
+ }
166
+ Err(e) => {
167
+ return Err(
168
+ Error::new(magnus::exception::runtime_error(), e.to_string())
169
+ )
170
+ }
171
+ }
172
+ }
173
+ Err(e) => {
174
+ return Err(
175
+ Error::new(magnus::exception::runtime_error(), e.to_string())
176
+ )
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ #[allow(clippy::let_unit_value, clippy::type_complexity)]
185
+ fn parse_args(args: &[Value]) -> Result<(Vec<String>, bool, bool, u64, bool, i64), Error> {
186
+ type KwArgBool = Option<Option<bool>>;
187
+ type KwArgU64 = Option<Option<u64>>;
188
+ type KwArgi64 = Option<Option<i64>>;
189
+
190
+ let args = scan_args(args)?;
191
+ let (paths,): (Vec<String>,) = args.required;
192
+ let _: () = args.optional;
193
+ let _: () = args.splat;
194
+ let _: () = args.trailing;
195
+ let _: () = args.block;
196
+
197
+ let kwargs = get_kwargs(
198
+ args.keywords,
199
+ &[],
200
+ &["recursive", "force_polling", "poll_interval", "ignore_remove", "debounce"],
201
+ )?;
202
+ let (recursive, force_polling, poll_interval, ignore_remove, debounce): (KwArgBool, KwArgBool, KwArgU64, KwArgBool, KwArgi64) =
203
+ kwargs.optional;
204
+ let _: () = kwargs.required;
205
+ let _: () = kwargs.splat;
206
+
207
+ Ok((
208
+ paths,
209
+ recursive.flatten().unwrap_or(false),
210
+ force_polling.flatten().unwrap_or(false),
211
+ poll_interval.flatten().unwrap_or(200),
212
+ ignore_remove.flatten().unwrap_or(false),
213
+ debounce.flatten().unwrap_or(-1),
214
+ ))
215
+ }
216
+ }
217
+
218
+ #[magnus::init]
219
+ fn init() -> Result<(), Error> {
220
+ let module = define_module("Watchcat")?;
221
+
222
+ let watcher_class = module.define_class("Watcher", object())?;
223
+ watcher_class.define_singleton_method("new", function!(WatchcatWatcher::new, 0))?;
224
+ watcher_class.define_method("watch", method!(WatchcatWatcher::watch, -1))?;
225
+ watcher_class.define_method("close", method!(WatchcatWatcher::close, 0))?;
226
+
227
+ Ok(())
228
+ }
@@ -0,0 +1,26 @@
1
+ module Watchcat
2
+ class Client
3
+ def initialize(uri, paths:, recursive:, force_polling:, poll_interval:, ignore_remove:, debounce:)
4
+ DRb.start_service
5
+ @watcher = Watchcat::Watcher.new
6
+ @server = DRbObject.new_with_uri(uri)
7
+ @paths = paths
8
+ @recursive = recursive
9
+ @force_polling = force_polling
10
+ @poll_interval = poll_interval
11
+ @ignore_remove = ignore_remove
12
+ @debounce = debounce
13
+ end
14
+
15
+ def run
16
+ @watcher.watch(
17
+ @paths,
18
+ recursive: @recursive,
19
+ force_polling: @force_polling,
20
+ poll_interval: @poll_interval,
21
+ ignore_remove: @ignore_remove,
22
+ debounce: @debounce
23
+ ) { |notification| @server.execute(notification) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,63 @@
1
+ require "watchcat/kind"
2
+
3
+ module Watchcat
4
+ class Event
5
+ attr_reader :kind, :paths, :raw_kind, :event
6
+
7
+ def initialize(kinds, paths, raw_kind)
8
+ @paths = paths
9
+ @raw_kind = raw_kind
10
+ build_kind(kinds)
11
+ end
12
+
13
+ def deconstruct_keys(_keys)
14
+ { paths: @paths, event: @event }
15
+ end
16
+
17
+ private
18
+
19
+ def build_kind(kinds)
20
+ @kind = Watchcat::EventKind.new
21
+ @event = kinds.shift
22
+ if event
23
+ @kind.public_send("#{event}=", Object.const_get("Watchcat::#{event.capitalize}Kind").new)
24
+ send("build_#{event}_kind", kinds)
25
+ else
26
+ @kind.any = Watchcat::AnyKind.new
27
+ if File.directory?(@paths.first)
28
+ @kind.any.kind = "folder"
29
+ else
30
+ @kind.any.kind = "file"
31
+ end
32
+ end
33
+ end
34
+
35
+ def build_access_kind(kinds)
36
+ @kind.access.kind = kinds.shift
37
+
38
+ if @kind.access.open? || @kind.access.close?
39
+ @kind.access.access_mode = Watchcat::AccessMode.new(kinds.shift)
40
+ end
41
+ end
42
+
43
+ def build_create_kind(kinds)
44
+ @kind.create.kind = kinds.shift
45
+ end
46
+
47
+ def build_modify_kind(kinds)
48
+ @kind.modify.kind = kinds.shift
49
+
50
+ if @kind.modify.data_change?
51
+ @kind.modify.data_change = Watchcat::DataChange.new(kinds.shift)
52
+ elsif @kind.modify.metadata?
53
+ @kind.modify.metadata = Watchcat::MetadataKind.new(kinds.shift)
54
+ elsif @kind.modify.rename?
55
+ @kind.modify.rename = Watchcat::RenameMode.new(kinds.shift)
56
+ end
57
+ end
58
+
59
+ def build_remove_kind(kinds)
60
+ @kind.remove.kind = kinds.shift
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ require "drb"
2
+ require "drb/unix"
3
+ require_relative "server"
4
+ require_relative "client"
5
+
6
+ module Watchcat
7
+ class Executor
8
+ def initialize(paths, recursive:, force_polling:, poll_interval:, wait_until_startup:, ignore_remove:, debounce:, block:)
9
+ @service = nil
10
+ @child_pid = nil
11
+ @paths = paths
12
+ @recursive = recursive
13
+ @force_polling = force_polling
14
+ @poll_interval = poll_interval
15
+ @wait_until_startup = wait_until_startup
16
+ @ignore_remove = ignore_remove
17
+ @debounce = debounce
18
+ @block = block
19
+ end
20
+
21
+ def start
22
+ server = Server.new(@block)
23
+ @service = DRb.start_service("drbunix:", server)
24
+ client = nil
25
+ client = build_client if @wait_until_startup
26
+
27
+ @child_pid = fork do
28
+ client = build_client unless @wait_until_startup
29
+ Process.setproctitle("watchcat: watcher")
30
+ client.run
31
+ end
32
+
33
+ main = Process.pid
34
+ at_exit do
35
+ @exit_status = $!.status if $!.is_a?(SystemExit)
36
+ stop if Process.pid == main
37
+ exit @exit_status if @exit_status
38
+ end
39
+ end
40
+
41
+ def stop
42
+ begin
43
+ Process.kill(:KILL, @child_pid)
44
+ rescue Errno::ESRCH
45
+ # NOTE: We can ignore this error because there process is already dead.
46
+ end
47
+ @service.stop_service
48
+ end
49
+
50
+ private
51
+
52
+ def build_client
53
+ Client.new(
54
+ @service.uri,
55
+ paths: @paths,
56
+ recursive: @recursive,
57
+ force_polling: @force_polling,
58
+ poll_interval: @poll_interval,
59
+ debounce: @debounce,
60
+ ignore_remove: @ignore_remove
61
+ )
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,200 @@
1
+ require "forwardable"
2
+
3
+ module Watchcat
4
+ class EventKind
5
+ attr_accessor :access, :create, :modify, :remove, :any
6
+
7
+ def initialize
8
+ @access, @create, @modify, @remove, @any = nil, nil, nil, nil,nil
9
+ end
10
+
11
+ def access?
12
+ !@access.nil?
13
+ end
14
+
15
+ def create?
16
+ !@create.nil?
17
+ end
18
+
19
+ def modify?
20
+ !@modify.nil?
21
+ end
22
+
23
+ def remove?
24
+ !@remove.nil?
25
+ end
26
+
27
+ def any?
28
+ !@any.nil?
29
+ end
30
+ end
31
+
32
+ class AccessKind
33
+ extend Forwardable
34
+
35
+ attr_accessor :kind, :access_mode
36
+ delegate [:excute_mode?, :read_mode?, :write_mode?] => :@access_mode
37
+
38
+ def initialize
39
+ @kind, @access_mode = nil, nil
40
+ end
41
+
42
+ def read?
43
+ @kind == "read"
44
+ end
45
+
46
+ def open?
47
+ @kind == "open"
48
+ end
49
+
50
+ def close?
51
+ @kind == "close"
52
+ end
53
+ end
54
+
55
+ class CreateKind
56
+ attr_accessor :kind
57
+
58
+ def file?
59
+ @kind == "file"
60
+ end
61
+
62
+ def folder?
63
+ @kind == "folder"
64
+ end
65
+ end
66
+
67
+ class ModifyKind
68
+ extend Forwardable
69
+
70
+ attr_accessor :kind, :data_change, :metadata, :rename
71
+ delegate [:size?, :content?] => :@data_change
72
+ delegate [:access_time?, :write_time?, :permission?, :ownership?, :extended?] => :@metadata
73
+ delegate [:from?, :to?, :both?] => :@rename
74
+
75
+ def initialize
76
+ @kind, @data_change, @metadata, @rename = nil, nil, nil, nil
77
+ end
78
+
79
+ def data_change?
80
+ @kind == "data_change"
81
+ end
82
+
83
+ def metadata?
84
+ @kind == "metadata"
85
+ end
86
+
87
+ def rename?
88
+ @kind == "rename"
89
+ end
90
+ end
91
+
92
+ class RemoveKind
93
+ attr_accessor :kind
94
+
95
+ def file?
96
+ @kind == "file"
97
+ end
98
+
99
+ def folder?
100
+ @kind == "folder"
101
+ end
102
+ end
103
+
104
+ class AnyKind
105
+ attr_accessor :kind
106
+
107
+ def file?
108
+ @kind == "file"
109
+ end
110
+
111
+ def folder?
112
+ @kind == "folder"
113
+ end
114
+ end
115
+
116
+
117
+ class AccessMode
118
+ attr_accessor :mode
119
+
120
+ def initialize(mode)
121
+ @mode = mode
122
+ end
123
+
124
+ def execute_mode?
125
+ @mode == "execute"
126
+ end
127
+
128
+ def read_mode?
129
+ @mode == "read"
130
+ end
131
+
132
+ def write_mode?
133
+ @mode == "write"
134
+ end
135
+ end
136
+
137
+ class DataChange
138
+ attr_accessor :kind
139
+
140
+ def initialize(kind)
141
+ @kind = kind
142
+ end
143
+
144
+ def size?
145
+ @kind == "size"
146
+ end
147
+
148
+ def content?
149
+ @kind == "content"
150
+ end
151
+ end
152
+
153
+ class MetadataKind
154
+ attr_accessor :kind
155
+
156
+ def initialize(kind)
157
+ @kind = kind
158
+ end
159
+
160
+ def access_time?
161
+ @kind == "access_time"
162
+ end
163
+
164
+ def write_time?
165
+ @kind == "write_time"
166
+ end
167
+
168
+ def permission?
169
+ @kind == "permission"
170
+ end
171
+
172
+ def ownership?
173
+ @kind == "ownership"
174
+ end
175
+
176
+ def extended?
177
+ @kind == "extended"
178
+ end
179
+ end
180
+
181
+ class RenameMode
182
+ attr_accessor :mode
183
+
184
+ def initialize(mode)
185
+ @mode = mode
186
+ end
187
+
188
+ def from?
189
+ @mode == "from"
190
+ end
191
+
192
+ def to?
193
+ @mode == "to"
194
+ end
195
+
196
+ def both?
197
+ @mode == "both"
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,14 @@
1
+ require "watchcat/event"
2
+
3
+ module Watchcat
4
+ class Server
5
+ def initialize(block)
6
+ @block = block
7
+ end
8
+
9
+ def execute(notification)
10
+ event = Watchcat::Event.new(notification[0], notification[1], notification[2])
11
+ @block.call(event)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Watchcat
2
+ VERSION = "0.2.0"
3
+ end
data/lib/watchcat.rb ADDED
@@ -0,0 +1,37 @@
1
+ require_relative "watchcat/version"
2
+ require_relative "watchcat/executor"
3
+
4
+ begin
5
+ require "watchcat/#{RUBY_VERSION.to_f}/watchcat"
6
+ rescue LoadError
7
+ require "watchcat/watchcat"
8
+ end
9
+
10
+ module Watchcat
11
+ class << self
12
+ def watch(
13
+ paths,
14
+ recursive: true,
15
+ force_polling: false,
16
+ poll_interval: nil,
17
+ wait_until_startup: false,
18
+ ignore_remove: false,
19
+ debounce: -1,
20
+ &block
21
+ )
22
+ w =
23
+ Watchcat::Executor.new(
24
+ Array(paths),
25
+ recursive: recursive,
26
+ force_polling: force_polling,
27
+ poll_interval: poll_interval,
28
+ wait_until_startup: wait_until_startup,
29
+ ignore_remove: ignore_remove,
30
+ debounce: debounce,
31
+ block: block
32
+ )
33
+ w.start
34
+ w
35
+ end
36
+ end
37
+ end
data/watchcat.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/watchcat/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "watchcat"
7
+ spec.version = Watchcat::VERSION
8
+ spec.authors = ["Yuji Yaginuma"]
9
+ spec.email = ["yuuji.yaginuma@gmail.com"]
10
+
11
+ spec.summary = "Simple filesystem notification library for Ruby. "
12
+ spec.homepage = "https://github.com/y-yagi/watchcat"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["rubygems_mfa_required"] = "true"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|benchmark)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.require_paths = ["lib"]
27
+ spec.extensions = ["ext/watchcat/extconf.rb"]
28
+
29
+ spec.add_dependency "rb_sys"
30
+ spec.add_dependency "drb"
31
+ spec.add_development_dependency "debug"
32
+ spec.add_development_dependency "minitest"
33
+ spec.add_development_dependency "minitest-retry"
34
+ spec.add_development_dependency "rake"
35
+ spec.add_development_dependency "rake-compiler"
36
+ spec.add_development_dependency "ruby_memcheck"
37
+ spec.add_development_dependency "listen"
38
+ end