cruise 0.1.0-x86_64-linux-gnu → 0.2.0-x86_64-linux-gnu
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.
- checksums.yaml +4 -4
- data/Cargo.lock +827 -0
- data/Cargo.toml +3 -0
- data/README.md +223 -0
- data/Rakefile +12 -5
- data/cruise.gemspec +11 -5
- data/ext/cruise/Cargo.toml +22 -0
- data/ext/cruise/build.rs +15 -0
- data/ext/cruise/cbindgen.toml +23 -0
- data/ext/cruise/cruise.c +158 -0
- data/ext/cruise/extconf.rb +113 -0
- data/ext/cruise/src/ffi.rs +124 -0
- data/ext/cruise/src/lib.rs +11 -0
- data/ext/cruise/src/watcher.rs +196 -0
- data/lib/cruise/3.2/cruise.so +0 -0
- data/lib/cruise/3.3/cruise.so +0 -0
- data/lib/cruise/3.4/cruise.so +0 -0
- data/lib/cruise/4.0/cruise.so +0 -0
- data/lib/cruise/event.rb +20 -0
- data/lib/cruise/version.rb +1 -1
- data/lib/cruise/watcher.rb +34 -0
- data/lib/cruise.rb +37 -6
- metadata +18 -5
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//! C ABI for Cruise.
|
|
2
|
+
//!
|
|
3
|
+
//! # Safety
|
|
4
|
+
//!
|
|
5
|
+
//! Pointer arguments must be valid (or null where documented). String arrays
|
|
6
|
+
//! are `(pointer, count)` pairs of NUL-terminated UTF-8 C strings. Any C string
|
|
7
|
+
//! returned through an out-parameter is heap-allocated by Rust and must be
|
|
8
|
+
//! released with [`cruise_string_free`]; the watcher handle with
|
|
9
|
+
//! [`cruise_watcher_free`]. The read fd returned by [`cruise_watcher_new`] is
|
|
10
|
+
//! transferred to the caller, who is responsible for closing it.
|
|
11
|
+
|
|
12
|
+
#![allow(clippy::missing_safety_doc)]
|
|
13
|
+
|
|
14
|
+
use std::ffi::{CStr, CString};
|
|
15
|
+
use std::os::raw::{c_char, c_int};
|
|
16
|
+
use std::ptr;
|
|
17
|
+
|
|
18
|
+
use crate::Watcher;
|
|
19
|
+
|
|
20
|
+
/// A single filesystem event handed to the C side. Both strings are owned by
|
|
21
|
+
/// the caller once populated and must be freed with [`cruise_string_free`].
|
|
22
|
+
#[repr(C)]
|
|
23
|
+
pub struct CruiseEvent {
|
|
24
|
+
pub path: *mut c_char,
|
|
25
|
+
pub kind: *mut c_char,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
unsafe fn c_string_array(pointer: *const *const c_char, count: usize) -> Vec<String> {
|
|
29
|
+
if pointer.is_null() || count == 0 {
|
|
30
|
+
return Vec::new();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
std::slice::from_raw_parts(pointer, count)
|
|
34
|
+
.iter()
|
|
35
|
+
.filter_map(|&entry| {
|
|
36
|
+
if entry.is_null() {
|
|
37
|
+
None
|
|
38
|
+
} else {
|
|
39
|
+
CStr::from_ptr(entry).to_str().ok().map(|value| value.to_string())
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.collect()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Create a watcher over `paths`, filtered by `globs` and `only_kinds`.
|
|
46
|
+
///
|
|
47
|
+
/// On success returns a heap-allocated handle and writes the read end of the
|
|
48
|
+
/// event pipe to `out_read_fd` (ownership transfers to the caller). On error
|
|
49
|
+
/// returns null and, when `out_error` is non-null, writes a message to be freed
|
|
50
|
+
/// with [`cruise_string_free`].
|
|
51
|
+
#[no_mangle]
|
|
52
|
+
pub unsafe extern "C" fn cruise_watcher_new(
|
|
53
|
+
paths: *const *const c_char,
|
|
54
|
+
path_count: usize,
|
|
55
|
+
debounce: f64,
|
|
56
|
+
globs: *const *const c_char,
|
|
57
|
+
glob_count: usize,
|
|
58
|
+
only_kinds: *const *const c_char,
|
|
59
|
+
only_count: usize,
|
|
60
|
+
out_read_fd: *mut c_int,
|
|
61
|
+
out_error: *mut *mut c_char,
|
|
62
|
+
) -> *mut Watcher {
|
|
63
|
+
let paths = c_string_array(paths, path_count);
|
|
64
|
+
let globs = c_string_array(globs, glob_count);
|
|
65
|
+
let only = c_string_array(only_kinds, only_count);
|
|
66
|
+
|
|
67
|
+
match Watcher::new(&paths, debounce, &globs, only) {
|
|
68
|
+
Ok((watcher, read_fd)) => {
|
|
69
|
+
if !out_read_fd.is_null() {
|
|
70
|
+
*out_read_fd = read_fd;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Box::into_raw(Box::new(watcher))
|
|
74
|
+
}
|
|
75
|
+
Err(message) => {
|
|
76
|
+
if !out_error.is_null() {
|
|
77
|
+
*out_error = CString::new(message).unwrap_or_default().into_raw();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ptr::null_mut()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Non-blocking: pop the next queued event into `out_event` and return true, or
|
|
86
|
+
/// return false if the queue is empty. Never touches Ruby and never blocks — the
|
|
87
|
+
/// C wrapper waits on the pipe fd instead.
|
|
88
|
+
#[no_mangle]
|
|
89
|
+
pub unsafe extern "C" fn cruise_watcher_poll(watcher: *mut Watcher, out_event: *mut CruiseEvent) -> bool {
|
|
90
|
+
if watcher.is_null() {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let watcher = &*watcher;
|
|
95
|
+
|
|
96
|
+
match watcher.poll() {
|
|
97
|
+
Some((path, kind)) => {
|
|
98
|
+
if !out_event.is_null() {
|
|
99
|
+
(*out_event).path = CString::new(path).unwrap_or_default().into_raw();
|
|
100
|
+
(*out_event).kind = CString::new(kind).unwrap_or_default().into_raw();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
true
|
|
104
|
+
}
|
|
105
|
+
None => false,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Free a watcher handle. Stops watching, joins the background thread, and
|
|
110
|
+
/// closes the write end of the pipe (the reader then observes EOF).
|
|
111
|
+
#[no_mangle]
|
|
112
|
+
pub unsafe extern "C" fn cruise_watcher_free(watcher: *mut Watcher) {
|
|
113
|
+
if !watcher.is_null() {
|
|
114
|
+
drop(Box::from_raw(watcher));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Free a string previously returned by this library.
|
|
119
|
+
#[no_mangle]
|
|
120
|
+
pub unsafe extern "C" fn cruise_string_free(s: *mut c_char) {
|
|
121
|
+
if !s.is_null() {
|
|
122
|
+
drop(CString::from_raw(s));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//! Cruise - a fast, OS-native file watcher.
|
|
2
|
+
//!
|
|
3
|
+
//! This crate contains the platform-agnostic watching logic ([`watcher`]) and a
|
|
4
|
+
//! C ABI ([`ffi`]) that a thin Ruby C extension links against. It deliberately
|
|
5
|
+
//! knows nothing about Ruby: it hands filtered `(path, kind)` pairs across the
|
|
6
|
+
//! FFI boundary and lets the C wrapper turn them into Ruby objects.
|
|
7
|
+
|
|
8
|
+
mod ffi;
|
|
9
|
+
mod watcher;
|
|
10
|
+
|
|
11
|
+
pub use watcher::Watcher;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
//! The platform-agnostic file watcher.
|
|
2
|
+
//!
|
|
3
|
+
//! Knows nothing about Ruby or the C ABI. It watches paths, applies the
|
|
4
|
+
//! configured kind/glob filters on the debouncer's background thread, and
|
|
5
|
+
//! pushes matching `(path, kind)` pairs onto a shared queue. A self-pipe is
|
|
6
|
+
//! written to on every batch so a consumer can wait on a file descriptor
|
|
7
|
+
//! (`IO#wait_readable`) instead of blocking a thread — which is what makes the
|
|
8
|
+
//! watcher cooperate with Ruby's fiber scheduler.
|
|
9
|
+
|
|
10
|
+
use std::any::Any;
|
|
11
|
+
use std::collections::VecDeque;
|
|
12
|
+
use std::os::fd::RawFd;
|
|
13
|
+
use std::os::raw::{c_int, c_void};
|
|
14
|
+
use std::path::PathBuf;
|
|
15
|
+
use std::sync::{Arc, Mutex};
|
|
16
|
+
use std::time::Duration;
|
|
17
|
+
|
|
18
|
+
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
19
|
+
use notify::event::{CreateKind, ModifyKind, RemoveKind};
|
|
20
|
+
use notify::{EventKind, RecursiveMode};
|
|
21
|
+
use notify_debouncer_full::{new_debouncer, DebounceEventResult};
|
|
22
|
+
|
|
23
|
+
struct Shared {
|
|
24
|
+
pending: Mutex<VecDeque<(String, String)>>,
|
|
25
|
+
glob_set: Option<GlobSet>,
|
|
26
|
+
only_kinds: Vec<String>,
|
|
27
|
+
write_fd: RawFd,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Shared {
|
|
31
|
+
fn enqueue(&self, event: notify::Event) -> bool {
|
|
32
|
+
let kind = event_kind_to_string(&event.kind);
|
|
33
|
+
|
|
34
|
+
if !self.only_kinds.is_empty() && !self.only_kinds.iter().any(|allowed| allowed == kind) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let mut queue = self.pending.lock().unwrap();
|
|
39
|
+
let mut added = false;
|
|
40
|
+
|
|
41
|
+
for path in event.paths {
|
|
42
|
+
if let Some(ref globs) = self.glob_set {
|
|
43
|
+
if !globs.is_match(&path) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
queue.push_back((path.to_string_lossy().into_owned(), kind.to_string()));
|
|
49
|
+
added = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
added
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn wake(&self) {
|
|
56
|
+
let byte = [1u8];
|
|
57
|
+
let _ = unsafe { libc::write(self.write_fd, byte.as_ptr() as *const c_void, 1) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Drop for Shared {
|
|
62
|
+
fn drop(&mut self) {
|
|
63
|
+
unsafe { libc::close(self.write_fd) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// A live filesystem watcher.
|
|
68
|
+
///
|
|
69
|
+
/// Holds the debouncer alive (dropping it stops watching) and the shared queue
|
|
70
|
+
/// the consumer drains. The read end of the self-pipe is handed to the caller
|
|
71
|
+
/// by [`Watcher::new`] and owned by them from that point on.
|
|
72
|
+
pub struct Watcher {
|
|
73
|
+
_debouncer: Box<dyn Any>,
|
|
74
|
+
shared: Arc<Shared>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl Watcher {
|
|
78
|
+
pub fn new(paths: &[String], debounce: f64, globs: &[String], only_kinds: Vec<String>) -> Result<(Self, RawFd), String> {
|
|
79
|
+
let glob_set = build_glob_set(globs)?;
|
|
80
|
+
let (read_fd, write_fd) = make_pipe()?;
|
|
81
|
+
|
|
82
|
+
let shared = Arc::new(Shared {
|
|
83
|
+
pending: Mutex::new(VecDeque::new()),
|
|
84
|
+
glob_set,
|
|
85
|
+
only_kinds,
|
|
86
|
+
write_fd,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
let callback_shared = Arc::clone(&shared);
|
|
90
|
+
|
|
91
|
+
let mut debouncer = match new_debouncer(Duration::from_secs_f64(debounce), None, move |result: DebounceEventResult| {
|
|
92
|
+
if let Ok(events) = result {
|
|
93
|
+
let mut added = false;
|
|
94
|
+
|
|
95
|
+
for debounced_event in events {
|
|
96
|
+
if callback_shared.enqueue(debounced_event.event) {
|
|
97
|
+
added = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if added {
|
|
102
|
+
callback_shared.wake();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}) {
|
|
106
|
+
Ok(debouncer) => debouncer,
|
|
107
|
+
|
|
108
|
+
Err(error) => {
|
|
109
|
+
unsafe { libc::close(read_fd) };
|
|
110
|
+
return Err(format!("Failed to create watcher: {error}"));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for path in paths {
|
|
115
|
+
let watch_path = PathBuf::from(path);
|
|
116
|
+
|
|
117
|
+
if !watch_path.exists() {
|
|
118
|
+
unsafe { libc::close(read_fd) };
|
|
119
|
+
return Err(format!("Path does not exist: {}", watch_path.display()));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if let Err(error) = debouncer.watch(&watch_path, RecursiveMode::Recursive) {
|
|
123
|
+
unsafe { libc::close(read_fd) };
|
|
124
|
+
return Err(format!("Failed to watch path: {error}"));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ok((
|
|
129
|
+
Watcher {
|
|
130
|
+
_debouncer: Box::new(debouncer),
|
|
131
|
+
shared,
|
|
132
|
+
},
|
|
133
|
+
read_fd,
|
|
134
|
+
))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn poll(&self) -> Option<(String, String)> {
|
|
138
|
+
self.shared.pending.lock().unwrap().pop_front()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn make_pipe() -> Result<(RawFd, RawFd), String> {
|
|
143
|
+
let mut fds = [0 as c_int; 2];
|
|
144
|
+
|
|
145
|
+
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
|
|
146
|
+
return Err("Failed to create event pipe".to_string());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set_nonblock_cloexec(fds[0]);
|
|
150
|
+
set_nonblock_cloexec(fds[1]);
|
|
151
|
+
|
|
152
|
+
Ok((fds[0], fds[1]))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn set_nonblock_cloexec(fd: c_int) {
|
|
156
|
+
unsafe {
|
|
157
|
+
let flags = libc::fcntl(fd, libc::F_GETFL);
|
|
158
|
+
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
|
159
|
+
|
|
160
|
+
let fd_flags = libc::fcntl(fd, libc::F_GETFD);
|
|
161
|
+
libc::fcntl(fd, libc::F_SETFD, fd_flags | libc::FD_CLOEXEC);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn build_glob_set(patterns: &[String]) -> Result<Option<GlobSet>, String> {
|
|
166
|
+
if patterns.is_empty() {
|
|
167
|
+
return Ok(None);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let mut builder = GlobSetBuilder::new();
|
|
171
|
+
|
|
172
|
+
for pattern in patterns {
|
|
173
|
+
let glob = Glob::new(pattern).map_err(|error| format!("Invalid glob pattern '{}': {}", pattern, error))?;
|
|
174
|
+
builder.add(glob);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let set = builder.build().map_err(|error| format!("Failed to build glob set: {}", error))?;
|
|
178
|
+
|
|
179
|
+
Ok(Some(set))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn event_kind_to_string(kind: &EventKind) -> &'static str {
|
|
183
|
+
match kind {
|
|
184
|
+
EventKind::Create(CreateKind::File) => "created",
|
|
185
|
+
EventKind::Create(CreateKind::Folder) => "created",
|
|
186
|
+
EventKind::Create(_) => "created",
|
|
187
|
+
EventKind::Modify(ModifyKind::Data(_)) => "modified",
|
|
188
|
+
EventKind::Modify(ModifyKind::Name(_)) => "renamed",
|
|
189
|
+
EventKind::Modify(_) => "modified",
|
|
190
|
+
EventKind::Remove(RemoveKind::File) => "removed",
|
|
191
|
+
EventKind::Remove(RemoveKind::Folder) => "removed",
|
|
192
|
+
EventKind::Remove(_) => "removed",
|
|
193
|
+
EventKind::Access(_) => "accessed",
|
|
194
|
+
EventKind::Any | EventKind::Other => "changed",
|
|
195
|
+
}
|
|
196
|
+
}
|
data/lib/cruise/3.2/cruise.so
CHANGED
|
Binary file
|
data/lib/cruise/3.3/cruise.so
CHANGED
|
Binary file
|
data/lib/cruise/3.4/cruise.so
CHANGED
|
Binary file
|
data/lib/cruise/4.0/cruise.so
CHANGED
|
Binary file
|
data/lib/cruise/event.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cruise
|
|
4
|
+
class Event
|
|
5
|
+
attr_reader :path, :kind
|
|
6
|
+
|
|
7
|
+
def initialize(path, kind)
|
|
8
|
+
@path = path
|
|
9
|
+
@kind = kind
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def inspect
|
|
13
|
+
"#<Cruise::Event kind=#{kind.inspect} path=#{path.inspect}>"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_s
|
|
17
|
+
"#{kind}: #{path}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/cruise/version.rb
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cruise
|
|
4
|
+
class Watcher
|
|
5
|
+
def initialize(*paths, glob: nil, debounce: DEFAULT_DEBOUNCE, only: nil)
|
|
6
|
+
paths = paths.flatten.grep(String)
|
|
7
|
+
|
|
8
|
+
raise ArgumentError, "Cruise::Watcher requires at least one path" if paths.empty?
|
|
9
|
+
|
|
10
|
+
glob_patterns = glob ? Array(glob) : []
|
|
11
|
+
only_kinds = only ? Array(only).map(&:to_s) : []
|
|
12
|
+
|
|
13
|
+
initialize_native(paths, debounce.to_f, glob_patterns, only_kinds)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def io
|
|
17
|
+
raise NotImplementedError, "Cruise::Watcher#io is provided by the native extension"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def poll
|
|
21
|
+
raise NotImplementedError, "Cruise::Watcher#poll is provided by the native extension"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
raise NotImplementedError, "Cruise::Watcher#close is provided by the native extension"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def initialize_native(_paths, _debounce, _globs, _only_kinds)
|
|
31
|
+
raise NotImplementedError, "Cruise::Watcher#initialize_native is provided by the native extension"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/cruise.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
3
5
|
require_relative "cruise/version"
|
|
6
|
+
require_relative "cruise/event"
|
|
7
|
+
require_relative "cruise/watcher"
|
|
4
8
|
|
|
5
9
|
begin
|
|
6
10
|
ruby_version = RUBY_VERSION.split(".")[0..1].join(".")
|
|
@@ -18,17 +22,44 @@ module Cruise
|
|
|
18
22
|
DEFAULT_DEBOUNCE = 0.1
|
|
19
23
|
|
|
20
24
|
class << self
|
|
21
|
-
|
|
25
|
+
# Watch one or more paths and yield a Cruise::Event for each change.
|
|
26
|
+
#
|
|
27
|
+
# Blocks until interrupted. Waiting happens on the watcher's pipe via
|
|
28
|
+
# IO#wait_readable, so this releases the GVL for other threads and, when a
|
|
29
|
+
# Fiber scheduler is set (e.g. inside Async), yields to other fibers instead
|
|
30
|
+
# of blocking the reactor.
|
|
31
|
+
def watch(*paths, glob: nil, debounce: DEFAULT_DEBOUNCE, only: nil, callback: nil, &block)
|
|
22
32
|
callback = block || callback
|
|
23
|
-
paths = args.flatten.grep(String)
|
|
24
33
|
|
|
25
|
-
raise ArgumentError, "Cruise.watch requires at least one path" if paths.empty?
|
|
26
34
|
raise ArgumentError, "Cruise.watch requires a block or callback" unless callback
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
watcher = Watcher.new(*paths, glob: glob, debounce: debounce, only: only)
|
|
37
|
+
io = watcher.io
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
loop do
|
|
41
|
+
io.wait_readable
|
|
42
|
+
closed = drain_wakeups(io)
|
|
43
|
+
|
|
44
|
+
while (event = watcher.poll)
|
|
45
|
+
callback.call(event)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
break if closed
|
|
49
|
+
end
|
|
50
|
+
ensure
|
|
51
|
+
watcher.close
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
def drain_wakeups(io)
|
|
58
|
+
loop { io.read_nonblock(4096) }
|
|
59
|
+
rescue IO::WaitReadable
|
|
60
|
+
false
|
|
61
|
+
rescue IOError
|
|
62
|
+
true
|
|
32
63
|
end
|
|
33
64
|
end
|
|
34
65
|
end
|
metadata
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cruise
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Marco Roth
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Cruise is a
|
|
14
|
-
|
|
13
|
+
description: Cruise is a file system watcher built on native OS events. Uses FSEvents
|
|
14
|
+
on macOS and inotify on Linux.
|
|
15
15
|
email:
|
|
16
16
|
- marco.roth@intergga.ch
|
|
17
17
|
executables: []
|
|
18
18
|
extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
|
20
20
|
files:
|
|
21
|
+
- Cargo.lock
|
|
22
|
+
- Cargo.toml
|
|
21
23
|
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
22
25
|
- Rakefile
|
|
23
26
|
- cruise.gemspec
|
|
27
|
+
- ext/cruise/Cargo.toml
|
|
28
|
+
- ext/cruise/build.rs
|
|
29
|
+
- ext/cruise/cbindgen.toml
|
|
30
|
+
- ext/cruise/cruise.c
|
|
31
|
+
- ext/cruise/extconf.rb
|
|
32
|
+
- ext/cruise/src/ffi.rs
|
|
33
|
+
- ext/cruise/src/lib.rs
|
|
34
|
+
- ext/cruise/src/watcher.rs
|
|
24
35
|
- lib/cruise.rb
|
|
25
36
|
- lib/cruise/3.2/cruise.so
|
|
26
37
|
- lib/cruise/3.3/cruise.so
|
|
27
38
|
- lib/cruise/3.4/cruise.so
|
|
28
39
|
- lib/cruise/4.0/cruise.so
|
|
40
|
+
- lib/cruise/event.rb
|
|
29
41
|
- lib/cruise/version.rb
|
|
42
|
+
- lib/cruise/watcher.rb
|
|
30
43
|
homepage: https://github.com/marcoroth/cruise
|
|
31
44
|
licenses:
|
|
32
45
|
- MIT
|
|
@@ -58,5 +71,5 @@ requirements: []
|
|
|
58
71
|
rubygems_version: 3.5.23
|
|
59
72
|
signing_key:
|
|
60
73
|
specification_version: 4
|
|
61
|
-
summary: A fast, native file watcher for Ruby
|
|
74
|
+
summary: A fast, OS-native file watcher for Ruby
|
|
62
75
|
test_files: []
|