cruise 0.0.0 → 0.2.0
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/LICENSE.txt +21 -0
- data/README.md +223 -0
- data/Rakefile +82 -5
- data/cruise.gemspec +16 -4
- 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/event.rb +20 -0
- data/lib/cruise/version.rb +1 -1
- data/lib/cruise/watcher.rb +34 -0
- data/lib/cruise.rb +59 -2
- metadata +21 -6
data/Cargo.toml
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marco Roth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Cruise</h1>
|
|
3
|
+
<h4>A fast, OS-native file watcher for Ruby.</h4>
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://rubygems.org/gems/cruise"><img alt="Gem Version" src="https://img.shields.io/gem/v/cruise"></a>
|
|
7
|
+
<a href="https://github.com/marcoroth/cruise/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/cruise"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p>A file system watcher built on native OS events, not polling.<br/>Uses FSEvents on macOS and inotify on Linux.</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
Cruise was originally built to power file watching in the [Herb](https://herb-tools.dev) dev server, none of the existing Ruby watchers were quite reliable enough for the realtime hot reloading that a local development server and workflow needs. It also works standalone for any project that needs to react to file changes.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
**Add to your Gemfile:**
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "cruise"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Or install directly:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install cruise
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Precompiled native gems are available for macOS and Linux. If a precompiled gem isn't available for your platform, it will compile from source (requires Rust).
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Basic Example
|
|
34
|
+
|
|
35
|
+
Watch a directory for changes:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "cruise"
|
|
39
|
+
|
|
40
|
+
Cruise.watch("app/views") do |event|
|
|
41
|
+
puts "#{event.kind}: #{event.path}"
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The callback receives a `Cruise::Event` with two attributes:
|
|
46
|
+
|
|
47
|
+
| Attribute | Description |
|
|
48
|
+
|--------------|----------------------------------------------------------------------------------------|
|
|
49
|
+
| `event.path` | Absolute path to the changed file |
|
|
50
|
+
| `event.kind` | One of: `"created"`, `"modified"`, `"renamed"`, `"removed"`, `"accessed"`, `"changed"` |
|
|
51
|
+
|
|
52
|
+
### Multiple Directories
|
|
53
|
+
|
|
54
|
+
Watch several paths at once:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Cruise.watch("app/views", "app/components", "lib/templates") do |event|
|
|
58
|
+
puts event.inspect
|
|
59
|
+
# => #<Cruise::Event kind="modified" path="/app/views/users/show.html.erb">
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Ruby Threads
|
|
64
|
+
|
|
65
|
+
Cruise releases the GVL while waiting for filesystem events, so Ruby threads run freely:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Thread.new { do_background_work }
|
|
69
|
+
|
|
70
|
+
Cruise.watch("src") do |event|
|
|
71
|
+
puts event
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Glob Filtering
|
|
76
|
+
|
|
77
|
+
Only receive events for files matching a pattern:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
|
|
81
|
+
puts event
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Multiple patterns:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Cruise.watch("app", glob: ["**/*.html.erb", "**/*.html"]) do |event|
|
|
89
|
+
puts event
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Debounce
|
|
94
|
+
|
|
95
|
+
Configure the debounce interval (default: 100ms):
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Cruise.watch("src", debounce: 0.5) do |event|
|
|
99
|
+
puts event
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Proc Callback
|
|
104
|
+
|
|
105
|
+
You can also pass a `Proc` via the `callback:` keyword:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
handler = proc { |event| puts event }
|
|
109
|
+
|
|
110
|
+
Cruise.watch("app/views", callback: handler)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Stopping
|
|
114
|
+
|
|
115
|
+
The watcher runs in a blocking loop. Use `Interrupt` to stop it cleanly:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
begin
|
|
119
|
+
Cruise.watch("app") do |event|
|
|
120
|
+
# process event
|
|
121
|
+
end
|
|
122
|
+
rescue Interrupt
|
|
123
|
+
puts "Stopped."
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Async / Fibers
|
|
128
|
+
|
|
129
|
+
`Cruise.watch` cooperates with Ruby's fiber scheduler, the API is identical whether you're on threads or fibers, with no separate "async mode." It waits on an internal pipe via `IO#wait_readable`, which yields to other fibers when a scheduler is active (for example inside [`Async`](https://github.com/socketry/async)) and otherwise blocks the thread with the GVL released.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require "async"
|
|
133
|
+
require "cruise"
|
|
134
|
+
|
|
135
|
+
Async do |task|
|
|
136
|
+
task.async do
|
|
137
|
+
Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
|
|
138
|
+
reload(event.path)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
task.async do
|
|
143
|
+
start_dev_server
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Cruise doesn't depend on `async`, it only relies on the standard `IO#wait_readable` hook, so any `Fiber::Scheduler` works.
|
|
149
|
+
|
|
150
|
+
#### Lower-level watcher
|
|
151
|
+
|
|
152
|
+
For custom event loops or manually-driven fibers, `Cruise::Watcher` exposes a readable `IO` and a non-blocking `#poll`:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
watcher = Cruise::Watcher.new("app/views", glob: "**/*.erb", debounce: 0.1)
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
loop do
|
|
159
|
+
watcher.io.wait_readable
|
|
160
|
+
watcher.io.read_nonblock(4096, exception: false)
|
|
161
|
+
|
|
162
|
+
while (event = watcher.poll)
|
|
163
|
+
handle(event)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
ensure
|
|
167
|
+
watcher.close
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`Cruise::Watcher.new` takes the same paths keyword arguments as `Cruise.watch`, it's the watcher `Cruise.watch` runs internally.
|
|
172
|
+
|
|
173
|
+
## How It Works
|
|
174
|
+
|
|
175
|
+
Cruise is a Ruby C extension that links a Rust static library (`libcruise.a`) built around the [notify](https://github.com/notify-rs/notify) crate. The Rust core exposes a small C ABI (generated with [cbindgen](https://github.com/mozilla/cbindgen)) and knows nothing about Ruby. A thin hand-written C wrapper bridges that ABI to Ruby objects via the Ruby C API.
|
|
176
|
+
|
|
177
|
+
1. `Cruise.watch` sets up a [notify](https://github.com/notify-rs/notify) watcher with event debouncing (100ms)
|
|
178
|
+
2. A background thread monitors filesystem events using the OS-native API
|
|
179
|
+
3. The main loop calls `rb_thread_call_without_gvl` to wait without blocking Ruby
|
|
180
|
+
4. When an event arrives, the GVL is re-acquired and your callback is invoked
|
|
181
|
+
|
|
182
|
+
### Platform Backends
|
|
183
|
+
|
|
184
|
+
| Platform | Backend | API |
|
|
185
|
+
|----------|----------|--------------------------|
|
|
186
|
+
| macOS | FSEvents | `CoreServices` framework |
|
|
187
|
+
| Linux | inotify | `inotify_init1` syscall |
|
|
188
|
+
|
|
189
|
+
All backends watch recursively by default.
|
|
190
|
+
|
|
191
|
+
## Prior Art
|
|
192
|
+
|
|
193
|
+
Cruise builds on ideas from the Ruby ecosystem's existing file watchers:
|
|
194
|
+
|
|
195
|
+
- **[`listen`](https://github.com/guard/listen)** the long-standing workhorse behind Guard, Rails, and much of the ecosystem. Wraps [rb-fsevent](https://github.com/guard/rb-fsevent) (macOS) and [rb-inotify](https://github.com/guard/rb-inotify) (Linux), with a polling fallback for other platforms and network shares.
|
|
196
|
+
- **[`guard`](https://github.com/guard/guard)** a command-line tool, built on listen, that runs tasks (tests, reloads, builds) in response to file changes.
|
|
197
|
+
- **[`io-watch`](https://github.com/socketry/io-watch)** a newer, deliberately minimal library from the [socketry](https://github.com/socketry) project, offering one simple, unified directory-watching interface across platforms without per-platform multiplexing in application code.
|
|
198
|
+
|
|
199
|
+
Cruise shares `io-watch`'s preference for a single, native-events-only interface over platform multiplexing. On top of that it focuses on what a hot-reload loop needs: built-in debouncing, glob and event-kind filtering, and precompiled native gems so there's nothing to build at install time on supported platforms. It deliberately has no polling adapter, so filesystems where native events aren't delivered fall outside its scope, reach for [listen](https://github.com/guard/listen) there.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
**Requirements:** Rust toolchain, Ruby 3.2+
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/marcoroth/cruise
|
|
207
|
+
cd cruise
|
|
208
|
+
bundle install
|
|
209
|
+
bundle exec rake compile
|
|
210
|
+
bundle exec rake test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Cross-compilation
|
|
214
|
+
|
|
215
|
+
Cruise uses [rake-compiler](https://github.com/rake-compiler/rake-compiler) and [rake-compiler-dock](https://github.com/rake-compiler/rake-compiler-dock) for building native gems:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
bundle exec rake gem:native
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
CHANGED
|
@@ -1,12 +1,89 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
require "
|
|
4
|
+
require "rake/testtask"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.libs << "ext"
|
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
11
|
+
end
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
begin
|
|
14
|
+
require "rake/extensiontask"
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
PLATFORMS = [
|
|
17
|
+
"aarch64-linux-gnu",
|
|
18
|
+
"aarch64-linux-musl",
|
|
19
|
+
"arm-linux-gnu",
|
|
20
|
+
"arm64-darwin",
|
|
21
|
+
"x86_64-darwin",
|
|
22
|
+
"x86_64-linux-gnu",
|
|
23
|
+
"x86_64-linux-musl"
|
|
24
|
+
].freeze
|
|
11
25
|
|
|
12
|
-
|
|
26
|
+
RB_SYS_PLATFORM_MAP = {
|
|
27
|
+
"aarch64-linux-gnu" => "aarch64-linux",
|
|
28
|
+
"aarch64-linux-musl" => "aarch64-linux-musl",
|
|
29
|
+
"arm-linux-gnu" => "arm-linux",
|
|
30
|
+
"arm64-darwin" => "arm64-darwin",
|
|
31
|
+
"x86_64-darwin" => "x86_64-darwin",
|
|
32
|
+
"x86_64-linux-gnu" => "x86_64-linux",
|
|
33
|
+
"x86_64-linux-musl" => "x86_64-linux-musl",
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
Rake::ExtensionTask.new do |ext|
|
|
37
|
+
ext.name = "cruise"
|
|
38
|
+
ext.source_pattern = "*.{c,h}"
|
|
39
|
+
ext.ext_dir = "ext/cruise"
|
|
40
|
+
ext.lib_dir = "lib/cruise"
|
|
41
|
+
ext.gem_spec = Gem::Specification.load("cruise.gemspec")
|
|
42
|
+
ext.cross_compile = true
|
|
43
|
+
ext.cross_platform = PLATFORMS
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
namespace "gem" do
|
|
47
|
+
task "prepare" do
|
|
48
|
+
require "rake_compiler_dock"
|
|
49
|
+
|
|
50
|
+
sh "bundle config set cache_all true"
|
|
51
|
+
|
|
52
|
+
gemspec_path = File.expand_path("./cruise.gemspec", __dir__)
|
|
53
|
+
spec = eval(File.read(gemspec_path), binding, gemspec_path)
|
|
54
|
+
|
|
55
|
+
RakeCompilerDock.set_ruby_cc_version(spec.required_ruby_version.as_list)
|
|
56
|
+
rescue LoadError
|
|
57
|
+
abort "rake_compiler_dock is required for this task"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
PLATFORMS.each do |platform|
|
|
61
|
+
desc "Build all native binary gems in parallel"
|
|
62
|
+
multitask "native" => platform
|
|
63
|
+
|
|
64
|
+
desc "Build the native gem for #{platform}"
|
|
65
|
+
task platform => "prepare" do
|
|
66
|
+
require "rb_sys"
|
|
67
|
+
|
|
68
|
+
rb_sys_platform = RB_SYS_PLATFORM_MAP.fetch(platform)
|
|
69
|
+
|
|
70
|
+
RakeCompilerDock.sh(
|
|
71
|
+
"export RCD_PLATFORM=#{platform} && " \
|
|
72
|
+
"bundle --local && rake native:#{platform} gem RUBY_CC_VERSION='#{ENV.fetch("RUBY_CC_VERSION", nil)}'",
|
|
73
|
+
platform: platform,
|
|
74
|
+
image: "rbsys/#{rb_sys_platform}:#{RbSys::VERSION}"
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
rescue LoadError => e
|
|
80
|
+
warn "WARNING: Failed to load extension tasks: #{e.message}"
|
|
81
|
+
|
|
82
|
+
desc "Compile task not available (rake-compiler not installed)"
|
|
83
|
+
task :compile do
|
|
84
|
+
abort "rake-compiler is required: #{e.message}\n\nRun: bundle install"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
task test: :compile
|
|
89
|
+
task default: [:compile, :test]
|
data/cruise.gemspec
CHANGED
|
@@ -8,21 +8,33 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Marco Roth"]
|
|
9
9
|
spec.email = ["marco.roth@intergga.ch"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "A fast, native file watcher for Ruby"
|
|
12
|
-
spec.description = "Cruise is a
|
|
11
|
+
spec.summary = "A fast, OS-native file watcher for Ruby"
|
|
12
|
+
spec.description = "Cruise is a file system watcher built on native OS events. Uses FSEvents on macOS and inotify on Linux."
|
|
13
13
|
spec.homepage = "https://github.com/marcoroth/cruise"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
16
|
-
spec.required_ruby_version = ">= 3.
|
|
16
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
17
17
|
spec.require_paths = ["lib"]
|
|
18
18
|
|
|
19
19
|
spec.files = Dir[
|
|
20
20
|
"cruise.gemspec",
|
|
21
21
|
"LICENSE.txt",
|
|
22
|
+
"README.md",
|
|
23
|
+
"Cargo.toml",
|
|
24
|
+
"Cargo.lock",
|
|
22
25
|
"Rakefile",
|
|
23
|
-
"lib/**/*.rb"
|
|
26
|
+
"lib/**/*.rb",
|
|
27
|
+
"ext/cruise/build.rs",
|
|
28
|
+
"ext/cruise/Cargo.toml",
|
|
29
|
+
"ext/cruise/cbindgen.toml",
|
|
30
|
+
"ext/cruise/cruise.c",
|
|
31
|
+
"ext/cruise/extconf.rb",
|
|
32
|
+
"ext/cruise/include/**/*.h",
|
|
33
|
+
"ext/cruise/src/**/*.rs"
|
|
24
34
|
]
|
|
25
35
|
|
|
36
|
+
spec.extensions = ["ext/cruise/extconf.rb"]
|
|
37
|
+
|
|
26
38
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
27
39
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
28
40
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "cruise"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
authors = ["Marco Roth <marco.roth@intergga.ch>"]
|
|
6
|
+
description = "A fast, OS-native file watcher for Ruby"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
publish = false
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "cruise"
|
|
12
|
+
path = "src/lib.rs"
|
|
13
|
+
crate-type = ["staticlib"]
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
notify = { version = "7", features = ["macos_fsevent"] }
|
|
17
|
+
notify-debouncer-full = "0.4"
|
|
18
|
+
globset = "0.4"
|
|
19
|
+
libc = "0.2"
|
|
20
|
+
|
|
21
|
+
[build-dependencies]
|
|
22
|
+
cbindgen = "0.28"
|
data/ext/cruise/build.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
fn main() {
|
|
6
|
+
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
7
|
+
let include_dir = PathBuf::from(&crate_dir).join("include");
|
|
8
|
+
let header_path = include_dir.join("cruise.h");
|
|
9
|
+
|
|
10
|
+
let _ = fs::create_dir_all(&include_dir);
|
|
11
|
+
|
|
12
|
+
if let Ok(bindings) = cbindgen::generate(&crate_dir) {
|
|
13
|
+
bindings.write_to_file(&header_path);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
language = "C"
|
|
2
|
+
header = """/* Generated by cbindgen, do not edit manually */
|
|
3
|
+
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
#include <stddef.h>"""
|
|
7
|
+
include_guard = "CRUISE_H"
|
|
8
|
+
no_includes = true
|
|
9
|
+
|
|
10
|
+
[export]
|
|
11
|
+
include = [
|
|
12
|
+
"CruiseEvent",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[enum]
|
|
16
|
+
rename_variants = "ScreamingSnakeCase"
|
|
17
|
+
prefix_with_name = true
|
|
18
|
+
|
|
19
|
+
[fn]
|
|
20
|
+
sort_by = "None"
|
|
21
|
+
|
|
22
|
+
[parse]
|
|
23
|
+
parse_deps = false
|
data/ext/cruise/cruise.c
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/encoding.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include "include/cruise.h"
|
|
5
|
+
|
|
6
|
+
static VALUE rb_mCruise;
|
|
7
|
+
static VALUE rb_cWatcher;
|
|
8
|
+
static VALUE rb_cEvent;
|
|
9
|
+
|
|
10
|
+
static VALUE make_utf8_string(const char *cstring) {
|
|
11
|
+
return rb_enc_str_new_cstr(cstring, rb_utf8_encoding());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static void fill_c_strings(VALUE array, const char **buffer) {
|
|
15
|
+
long length = RARRAY_LEN(array);
|
|
16
|
+
|
|
17
|
+
for (long index = 0; index < length; index++) {
|
|
18
|
+
VALUE string = rb_ary_entry(array, index);
|
|
19
|
+
|
|
20
|
+
buffer[index] = StringValueCStr(string);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static void watcher_free(void *pointer) {
|
|
25
|
+
if (pointer) {
|
|
26
|
+
cruise_watcher_free((struct Watcher *) pointer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static size_t watcher_size(const void *pointer) {
|
|
31
|
+
(void) pointer;
|
|
32
|
+
|
|
33
|
+
return sizeof(void *);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static const rb_data_type_t watcher_type = {
|
|
37
|
+
.wrap_struct_name = "Cruise::Watcher",
|
|
38
|
+
.function = {
|
|
39
|
+
.dfree = watcher_free,
|
|
40
|
+
.dsize = watcher_size,
|
|
41
|
+
},
|
|
42
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
static VALUE watcher_alloc(VALUE klass) {
|
|
46
|
+
return TypedData_Wrap_Struct(klass, &watcher_type, NULL);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static struct Watcher *get_watcher(VALUE self) {
|
|
50
|
+
struct Watcher *watcher;
|
|
51
|
+
|
|
52
|
+
TypedData_Get_Struct(self, struct Watcher, &watcher_type, watcher);
|
|
53
|
+
|
|
54
|
+
return watcher;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static VALUE watcher_initialize_native(VALUE self, VALUE paths, VALUE debounce, VALUE globs, VALUE only_kinds) {
|
|
58
|
+
long path_count = RARRAY_LEN(paths);
|
|
59
|
+
long glob_count = RARRAY_LEN(globs);
|
|
60
|
+
long only_count = RARRAY_LEN(only_kinds);
|
|
61
|
+
|
|
62
|
+
const char **path_pointers = path_count > 0 ? ALLOCA_N(const char *, path_count) : NULL;
|
|
63
|
+
const char **glob_pointers = glob_count > 0 ? ALLOCA_N(const char *, glob_count) : NULL;
|
|
64
|
+
const char **only_pointers = only_count > 0 ? ALLOCA_N(const char *, only_count) : NULL;
|
|
65
|
+
|
|
66
|
+
if (path_pointers) fill_c_strings(paths, path_pointers);
|
|
67
|
+
if (glob_pointers) fill_c_strings(globs, glob_pointers);
|
|
68
|
+
if (only_pointers) fill_c_strings(only_kinds, only_pointers);
|
|
69
|
+
|
|
70
|
+
int read_fd = -1;
|
|
71
|
+
char *error = NULL;
|
|
72
|
+
|
|
73
|
+
struct Watcher *watcher = cruise_watcher_new(
|
|
74
|
+
path_pointers, (size_t) path_count,
|
|
75
|
+
NUM2DBL(debounce),
|
|
76
|
+
glob_pointers, (size_t) glob_count,
|
|
77
|
+
only_pointers, (size_t) only_count,
|
|
78
|
+
&read_fd,
|
|
79
|
+
&error
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!watcher) {
|
|
83
|
+
VALUE message = error ? make_utf8_string(error) : rb_str_new_cstr("Failed to create watcher");
|
|
84
|
+
|
|
85
|
+
if (error) cruise_string_free(error);
|
|
86
|
+
|
|
87
|
+
const char *message_cstr = StringValueCStr(message);
|
|
88
|
+
VALUE error_class = rb_eRuntimeError;
|
|
89
|
+
|
|
90
|
+
if (strstr(message_cstr, "does not exist") || strstr(message_cstr, "Invalid glob pattern")) {
|
|
91
|
+
error_class = rb_eArgError;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rb_raise(error_class, "%s", message_cstr);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
RTYPEDDATA_DATA(self) = watcher;
|
|
98
|
+
|
|
99
|
+
VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(read_fd), rb_str_new_cstr("r"));
|
|
100
|
+
rb_iv_set(self, "@io", io);
|
|
101
|
+
|
|
102
|
+
return self;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// watcher.io
|
|
106
|
+
static VALUE watcher_io(VALUE self) {
|
|
107
|
+
return rb_iv_get(self, "@io");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// watcher.poll
|
|
111
|
+
static VALUE watcher_poll(VALUE self) {
|
|
112
|
+
struct Watcher *watcher = get_watcher(self);
|
|
113
|
+
|
|
114
|
+
if (!watcher) return Qnil;
|
|
115
|
+
|
|
116
|
+
CruiseEvent event = { NULL, NULL };
|
|
117
|
+
|
|
118
|
+
if (!cruise_watcher_poll(watcher, &event)) return Qnil;
|
|
119
|
+
|
|
120
|
+
VALUE path = make_utf8_string(event.path);
|
|
121
|
+
VALUE kind = make_utf8_string(event.kind);
|
|
122
|
+
|
|
123
|
+
cruise_string_free(event.path);
|
|
124
|
+
cruise_string_free(event.kind);
|
|
125
|
+
|
|
126
|
+
return rb_funcall(rb_cEvent, rb_intern("new"), 2, path, kind);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// watcher.close
|
|
130
|
+
static VALUE watcher_close(VALUE self) {
|
|
131
|
+
struct Watcher *watcher = get_watcher(self);
|
|
132
|
+
|
|
133
|
+
if (watcher) {
|
|
134
|
+
cruise_watcher_free(watcher);
|
|
135
|
+
RTYPEDDATA_DATA(self) = NULL;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
VALUE io = rb_iv_get(self, "@io");
|
|
139
|
+
|
|
140
|
+
if (!NIL_P(io) && !RTEST(rb_funcall(io, rb_intern("closed?"), 0))) {
|
|
141
|
+
rb_funcall(io, rb_intern("close"), 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Qnil;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void Init_cruise(void) {
|
|
148
|
+
rb_mCruise = rb_define_module("Cruise");
|
|
149
|
+
rb_cEvent = rb_const_get(rb_mCruise, rb_intern("Event"));
|
|
150
|
+
|
|
151
|
+
rb_cWatcher = rb_define_class_under(rb_mCruise, "Watcher", rb_cObject);
|
|
152
|
+
|
|
153
|
+
rb_define_alloc_func(rb_cWatcher, watcher_alloc);
|
|
154
|
+
rb_define_private_method(rb_cWatcher, "initialize_native", watcher_initialize_native, 4);
|
|
155
|
+
rb_define_method(rb_cWatcher, "io", watcher_io, 0);
|
|
156
|
+
rb_define_method(rb_cWatcher, "poll", watcher_poll, 0);
|
|
157
|
+
rb_define_method(rb_cWatcher, "close", watcher_close, 0);
|
|
158
|
+
}
|