itsi-scheduler 0.1.5 → 0.1.14
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.
Potentially problematic release.
This version of itsi-scheduler might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +83 -22
- data/README.md +5 -0
- data/_index.md +7 -0
- data/ext/itsi_error/src/from.rs +26 -29
- data/ext/itsi_error/src/lib.rs +10 -1
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_rb_helpers/Cargo.toml +1 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/ext/itsi_rb_helpers/src/lib.rs +59 -9
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +69 -26
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +81 -75
- data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
- data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
- data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +108 -103
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +79 -38
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
- data/ext/itsi_server/src/server/bind.rs +33 -20
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/cache_store.rs +74 -0
- data/ext/itsi_server/src/server/itsi_service.rs +172 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/listener.rs +197 -106
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +264 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
- data/ext/itsi_server/src/server/mod.rs +8 -1
- data/ext/itsi_server/src/server/process_worker.rs +44 -11
- data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +129 -46
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +337 -163
- data/ext/itsi_server/src/server/signal.rs +25 -2
- data/ext/itsi_server/src/server/static_file_server.rs +984 -0
- data/ext/itsi_server/src/server/thread_worker.rs +164 -88
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +55 -17
- data/ext/itsi_server/src/server/tls.rs +104 -28
- data/ext/itsi_server/src/server/types.rs +43 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +222 -34
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +2 -2
- metadata +79 -14
- data/ext/itsi_server/extconf.rb +0 -6
- data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/response/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
- data/ext/itsi_server/src/server/itsi_server.rs +0 -244
@@ -13,7 +13,7 @@ use itsi_tracing::error;
|
|
13
13
|
use magnus::error::Result as MagnusResult;
|
14
14
|
use parking_lot::RwLock;
|
15
15
|
use std::{
|
16
|
-
|
16
|
+
collections::HashMap,
|
17
17
|
io,
|
18
18
|
os::{fd::FromRawFd, unix::net::UnixStream},
|
19
19
|
str::FromStr,
|
@@ -31,30 +31,32 @@ use tokio_stream::wrappers::ReceiverStream;
|
|
31
31
|
use tokio_util::io::ReaderStream;
|
32
32
|
use tracing::warn;
|
33
33
|
|
34
|
-
use crate::server::
|
34
|
+
use crate::server::{
|
35
|
+
byte_frame::ByteFrame, serve_strategy::single_mode::RunningPhase, types::HttpResponse,
|
36
|
+
};
|
35
37
|
|
36
|
-
#[magnus::wrap(class = "Itsi::
|
38
|
+
#[magnus::wrap(class = "Itsi::HttpResponse", free_immediately, size)]
|
37
39
|
#[derive(Debug, Clone)]
|
38
|
-
pub struct
|
40
|
+
pub struct ItsiHttpResponse {
|
39
41
|
pub data: Arc<ResponseData>,
|
40
42
|
}
|
41
43
|
|
42
44
|
#[derive(Debug)]
|
43
45
|
pub struct ResponseData {
|
44
|
-
pub response: RwLock<Option<
|
45
|
-
pub response_writer: RwLock<Option<mpsc::Sender<
|
46
|
+
pub response: RwLock<Option<HttpResponse>>,
|
47
|
+
pub response_writer: RwLock<Option<mpsc::Sender<ByteFrame>>>,
|
46
48
|
pub response_buffer: RwLock<BytesMut>,
|
47
49
|
pub hijacked_socket: RwLock<Option<UnixStream>>,
|
48
50
|
pub parts: Parts,
|
49
51
|
}
|
50
52
|
|
51
|
-
impl
|
53
|
+
impl ItsiHttpResponse {
|
52
54
|
pub async fn build(
|
53
55
|
&self,
|
54
|
-
first_frame:
|
55
|
-
receiver: mpsc::Receiver<
|
56
|
+
first_frame: ByteFrame,
|
57
|
+
receiver: mpsc::Receiver<ByteFrame>,
|
56
58
|
shutdown_rx: watch::Receiver<RunningPhase>,
|
57
|
-
) ->
|
59
|
+
) -> HttpResponse {
|
58
60
|
if self.is_hijacked() {
|
59
61
|
return match self.process_hijacked_response().await {
|
60
62
|
Ok(result) => result,
|
@@ -64,31 +66,30 @@ impl ItsiResponse {
|
|
64
66
|
}
|
65
67
|
};
|
66
68
|
}
|
67
|
-
|
68
69
|
let mut response = self.data.response.write().take().unwrap();
|
69
|
-
*response.body_mut() = if first_frame
|
70
|
+
*response.body_mut() = if matches!(first_frame, ByteFrame::Empty) {
|
70
71
|
BoxBody::new(Empty::new())
|
71
|
-
} else if
|
72
|
-
BoxBody::new(Full::new(first_frame.
|
72
|
+
} else if matches!(first_frame, ByteFrame::End(_)) {
|
73
|
+
BoxBody::new(Full::new(first_frame.into()))
|
73
74
|
} else {
|
74
|
-
let initial_frame = tokio_stream::once(Ok(Frame::data(first_frame
|
75
|
+
let initial_frame = tokio_stream::once(Ok(Frame::data(Bytes::from(first_frame))));
|
75
76
|
let frame_stream = unfold(
|
76
77
|
(ReceiverStream::new(receiver), shutdown_rx),
|
77
78
|
|(mut receiver, mut shutdown_rx)| async move {
|
78
79
|
if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
|
79
|
-
warn!("Disconnecting streaming client.");
|
80
80
|
return None;
|
81
81
|
}
|
82
82
|
loop {
|
83
83
|
tokio::select! {
|
84
84
|
maybe_bytes = receiver.next() => {
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
} else {
|
89
|
-
// Receiver closed, end the stream.
|
90
|
-
return None;
|
85
|
+
match maybe_bytes {
|
86
|
+
Some(ByteFrame::Data(bytes)) | Some(ByteFrame::End(bytes)) => {
|
87
|
+
return Some((Ok(Frame::data(bytes)), (receiver, shutdown_rx)));
|
91
88
|
}
|
89
|
+
_ => {
|
90
|
+
return None;
|
91
|
+
}
|
92
|
+
}
|
92
93
|
},
|
93
94
|
_ = shutdown_rx.changed() => {
|
94
95
|
match *shutdown_rx.borrow() {
|
@@ -184,7 +185,7 @@ impl ItsiResponse {
|
|
184
185
|
Ok((headers, status, requires_upgrade, reader))
|
185
186
|
}
|
186
187
|
|
187
|
-
pub async fn process_hijacked_response(&self) -> Result<
|
188
|
+
pub async fn process_hijacked_response(&self) -> Result<HttpResponse> {
|
188
189
|
let (headers, status, requires_upgrade, reader) = self.read_hijacked_headers().await?;
|
189
190
|
let mut response = if requires_upgrade {
|
190
191
|
let parts = self.data.parts.clone();
|
@@ -261,27 +262,31 @@ impl ItsiResponse {
|
|
261
262
|
}
|
262
263
|
}
|
263
264
|
|
264
|
-
pub fn send_frame(&self, frame: Bytes) -> MagnusResult<
|
265
|
-
self.send_frame_into(frame, &self.data.response_writer)
|
265
|
+
pub fn send_frame(&self, frame: Bytes) -> MagnusResult<()> {
|
266
|
+
self.send_frame_into(ByteFrame::Data(frame), &self.data.response_writer)
|
267
|
+
}
|
268
|
+
|
269
|
+
pub fn recv_frame(&self) {
|
270
|
+
// not implemented
|
266
271
|
}
|
267
272
|
|
268
|
-
pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<
|
269
|
-
let result = self.send_frame_into(frame, &self.data.response_writer);
|
273
|
+
pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<()> {
|
274
|
+
let result = self.send_frame_into(ByteFrame::End(frame), &self.data.response_writer);
|
270
275
|
self.data.response_writer.write().take();
|
271
276
|
result
|
272
277
|
}
|
273
278
|
|
274
279
|
pub fn send_frame_into(
|
275
280
|
&self,
|
276
|
-
frame:
|
277
|
-
writer: &RwLock<Option<mpsc::Sender<
|
278
|
-
) -> MagnusResult<
|
281
|
+
frame: ByteFrame,
|
282
|
+
writer: &RwLock<Option<mpsc::Sender<ByteFrame>>>,
|
283
|
+
) -> MagnusResult<()> {
|
279
284
|
if let Some(writer) = writer.write().as_ref() {
|
280
|
-
writer
|
281
|
-
.blocking_send(
|
282
|
-
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
|
285
|
+
return Ok(writer
|
286
|
+
.blocking_send(frame)
|
287
|
+
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?);
|
283
288
|
}
|
284
|
-
Ok(
|
289
|
+
Ok(())
|
285
290
|
}
|
286
291
|
|
287
292
|
pub fn is_hijacked(&self) -> bool {
|
@@ -292,12 +297,28 @@ impl ItsiResponse {
|
|
292
297
|
self.data.response_writer.write().take();
|
293
298
|
Ok(true)
|
294
299
|
}
|
300
|
+
fn accept_str(&self) -> &str {
|
301
|
+
self.data
|
302
|
+
.parts
|
303
|
+
.headers
|
304
|
+
.get("Content-Type")
|
305
|
+
.and_then(|hv| hv.to_str().ok()) // handle invalid utf-8
|
306
|
+
.unwrap_or("application/x-www-form-urlencoded")
|
307
|
+
}
|
308
|
+
|
309
|
+
pub fn is_html(&self) -> bool {
|
310
|
+
self.accept_str().starts_with("text/html")
|
311
|
+
}
|
312
|
+
|
313
|
+
pub fn is_json(&self) -> bool {
|
314
|
+
self.accept_str().starts_with("application/json")
|
315
|
+
}
|
295
316
|
|
296
317
|
pub fn close_read(&self) -> MagnusResult<bool> {
|
297
|
-
|
318
|
+
Ok(true)
|
298
319
|
}
|
299
320
|
|
300
|
-
pub fn new(parts: Parts, response_writer: mpsc::Sender<
|
321
|
+
pub fn new(parts: Parts, response_writer: mpsc::Sender<ByteFrame>) -> Self {
|
301
322
|
Self {
|
302
323
|
data: Arc::new(ResponseData {
|
303
324
|
response: RwLock::new(Some(Response::new(BoxBody::new(Empty::new())))),
|
@@ -320,6 +341,26 @@ impl ItsiResponse {
|
|
320
341
|
Ok(())
|
321
342
|
}
|
322
343
|
|
344
|
+
pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
|
345
|
+
if let Some(ref mut resp) = *self.data.response.write() {
|
346
|
+
let headers_mut = resp.headers_mut();
|
347
|
+
for (name, values) in headers {
|
348
|
+
let header_name = HeaderName::from_bytes(&name).map_err(|e| {
|
349
|
+
itsi_error::ItsiError::InvalidInput(format!(
|
350
|
+
"Invalid header name {:?}: {:?}",
|
351
|
+
name, e
|
352
|
+
))
|
353
|
+
})?;
|
354
|
+
for value in values {
|
355
|
+
let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
|
356
|
+
headers_mut.insert(&header_name, header_value);
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
Ok(())
|
362
|
+
}
|
363
|
+
|
323
364
|
pub fn set_status(&self, status: u16) -> MagnusResult<()> {
|
324
365
|
if let Some(ref mut resp) = *self.data.response.write() {
|
325
366
|
*resp.status_mut() = StatusCode::from_u16(status).map_err(|e| {
|
@@ -338,8 +379,8 @@ impl ItsiResponse {
|
|
338
379
|
*self.data.hijacked_socket.write() = Some(stream);
|
339
380
|
if let Some(writer) = self.data.response_writer.write().as_ref() {
|
340
381
|
writer
|
341
|
-
.blocking_send(
|
342
|
-
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
|
382
|
+
.blocking_send(ByteFrame::Empty)
|
383
|
+
.map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?
|
343
384
|
}
|
344
385
|
self.close();
|
345
386
|
Ok(())
|
@@ -0,0 +1,225 @@
|
|
1
|
+
use derive_more::Debug;
|
2
|
+
use globset::{Glob, GlobSet, GlobSetBuilder};
|
3
|
+
use magnus::error::Result;
|
4
|
+
use nix::unistd::{close, fork, pipe, read};
|
5
|
+
use notify::{event::ModifyKind, EventKind, RecommendedWatcher};
|
6
|
+
use notify::{Event, RecursiveMode, Watcher};
|
7
|
+
use std::path::Path;
|
8
|
+
use std::sync::mpsc::Sender;
|
9
|
+
use std::time::{Duration, Instant};
|
10
|
+
use std::{collections::HashSet, fs};
|
11
|
+
use std::{
|
12
|
+
os::fd::{AsRawFd, IntoRawFd, OwnedFd},
|
13
|
+
path::PathBuf,
|
14
|
+
process::Command,
|
15
|
+
sync::mpsc,
|
16
|
+
thread::{self},
|
17
|
+
};
|
18
|
+
|
19
|
+
/// Represents a set of patterns and commands.
|
20
|
+
#[derive(Debug, Clone)]
|
21
|
+
struct PatternGroup {
|
22
|
+
base_dir: PathBuf,
|
23
|
+
glob_set: GlobSet,
|
24
|
+
pattern: String,
|
25
|
+
commands: Vec<Vec<String>>,
|
26
|
+
last_triggered: Option<Instant>,
|
27
|
+
}
|
28
|
+
|
29
|
+
/// Extracts the base directory from a wildcard pattern by taking the portion up to the first
|
30
|
+
/// component that contains a wildcard character.
|
31
|
+
fn extract_and_canonicalize_base_dir(pattern: &str) -> PathBuf {
|
32
|
+
if !(pattern.contains("*") || pattern.contains("?") || pattern.contains('[')) {
|
33
|
+
if let Ok(metadata) = fs::metadata(pattern) {
|
34
|
+
if metadata.is_dir() {
|
35
|
+
return fs::canonicalize(pattern).unwrap();
|
36
|
+
}
|
37
|
+
if metadata.is_file() {
|
38
|
+
return fs::canonicalize(pattern)
|
39
|
+
.unwrap()
|
40
|
+
.parent()
|
41
|
+
.unwrap()
|
42
|
+
.to_path_buf();
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
let path = Path::new(pattern);
|
48
|
+
let mut base = PathBuf::new();
|
49
|
+
for comp in path.components() {
|
50
|
+
let comp_str = comp.as_os_str().to_string_lossy();
|
51
|
+
if comp_str.contains('*') || comp_str.contains('?') || comp_str.contains('[') {
|
52
|
+
break;
|
53
|
+
} else {
|
54
|
+
base.push(comp);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
// If no base was built, default to "."
|
58
|
+
let base = if base.as_os_str().is_empty() || !base.exists() {
|
59
|
+
PathBuf::from(".")
|
60
|
+
} else {
|
61
|
+
base
|
62
|
+
};
|
63
|
+
|
64
|
+
// Canonicalize to get the absolute path.
|
65
|
+
fs::canonicalize(&base).unwrap_or(base)
|
66
|
+
}
|
67
|
+
|
68
|
+
/// Minimum time between triggering the same pattern group (debounce time)
|
69
|
+
const DEBOUNCE_DURATION: Duration = Duration::from_millis(500);
|
70
|
+
|
71
|
+
pub fn watch_groups(pattern_groups: Vec<(String, Vec<Vec<String>>)>) -> Result<Option<OwnedFd>> {
|
72
|
+
let (r_fd, w_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
|
73
|
+
magnus::Error::new(
|
74
|
+
magnus::exception::exception(),
|
75
|
+
format!("Failed to create watcher pipe: {}", e),
|
76
|
+
)
|
77
|
+
})?;
|
78
|
+
|
79
|
+
let fork_result = unsafe {
|
80
|
+
fork().map_err(|e| {
|
81
|
+
magnus::Error::new(
|
82
|
+
magnus::exception::exception(),
|
83
|
+
format!("Failed to fork file watcher: {}", e),
|
84
|
+
)
|
85
|
+
})
|
86
|
+
}?;
|
87
|
+
|
88
|
+
if fork_result.is_child() {
|
89
|
+
let _ = close(w_fd.into_raw_fd());
|
90
|
+
thread::spawn(move || {
|
91
|
+
let mut buf = [0u8; 1];
|
92
|
+
loop {
|
93
|
+
match read(r_fd.as_raw_fd(), &mut buf) {
|
94
|
+
Ok(0) => {
|
95
|
+
std::process::exit(0);
|
96
|
+
}
|
97
|
+
Ok(_) => {}
|
98
|
+
Err(_) => {
|
99
|
+
std::process::exit(0);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
});
|
104
|
+
|
105
|
+
let mut groups = Vec::new();
|
106
|
+
for (pattern, commands) in pattern_groups.into_iter() {
|
107
|
+
let base_dir = extract_and_canonicalize_base_dir(&pattern);
|
108
|
+
let glob = Glob::new(&pattern).map_err(|e| {
|
109
|
+
magnus::Error::new(
|
110
|
+
magnus::exception::exception(),
|
111
|
+
format!("Failed to create watch glob: {}", e),
|
112
|
+
)
|
113
|
+
})?;
|
114
|
+
let glob_set = GlobSetBuilder::new().add(glob).build().map_err(|e| {
|
115
|
+
magnus::Error::new(
|
116
|
+
magnus::exception::exception(),
|
117
|
+
format!("Failed to create watch glob set: {}", e),
|
118
|
+
)
|
119
|
+
})?;
|
120
|
+
groups.push(PatternGroup {
|
121
|
+
base_dir,
|
122
|
+
glob_set,
|
123
|
+
pattern,
|
124
|
+
commands,
|
125
|
+
last_triggered: None,
|
126
|
+
});
|
127
|
+
}
|
128
|
+
|
129
|
+
// Create a channel and a watcher.
|
130
|
+
let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
|
131
|
+
let sender = tx.clone();
|
132
|
+
fn event_fn(sender: Sender<notify::Result<Event>>) -> impl Fn(notify::Result<Event>) {
|
133
|
+
move |res| match res {
|
134
|
+
Ok(event) => {
|
135
|
+
sender.send(Ok(event)).unwrap();
|
136
|
+
}
|
137
|
+
Err(e) => println!("watch error: {:?}", e),
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
let mut watched_dirs = HashSet::new();
|
142
|
+
let mut watcher: RecommendedWatcher =
|
143
|
+
notify::recommended_watcher(event_fn(sender)).expect("Failed to create watcher");
|
144
|
+
for group in &groups {
|
145
|
+
if watched_dirs.insert(group.base_dir.clone()) {
|
146
|
+
watcher
|
147
|
+
.watch(&group.base_dir, RecursiveMode::Recursive)
|
148
|
+
.expect("Failed to add watch");
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
// Main event loop.
|
153
|
+
for res in rx {
|
154
|
+
match res {
|
155
|
+
Ok(event) => {
|
156
|
+
if !matches!(event.kind, EventKind::Modify(ModifyKind::Metadata(_))) {
|
157
|
+
continue;
|
158
|
+
}
|
159
|
+
let now = Instant::now();
|
160
|
+
for group in &mut groups {
|
161
|
+
for path in event.paths.iter() {
|
162
|
+
if let Ok(rel_path) = path.strip_prefix(&group.base_dir) {
|
163
|
+
if group.glob_set.is_match(rel_path)
|
164
|
+
|| rel_path.to_str().is_some_and(|s| s == group.pattern)
|
165
|
+
{
|
166
|
+
// Check if we should debounce this event
|
167
|
+
if let Some(last_triggered) = group.last_triggered {
|
168
|
+
if now.duration_since(last_triggered) < DEBOUNCE_DURATION {
|
169
|
+
// Skip this event as we've recently triggered for this pattern
|
170
|
+
continue;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
// Update the last triggered time
|
175
|
+
group.last_triggered = Some(now);
|
176
|
+
|
177
|
+
// Execute the commands for this group.
|
178
|
+
for command in &group.commands {
|
179
|
+
if command.is_empty() {
|
180
|
+
continue;
|
181
|
+
}
|
182
|
+
let mut cmd = Command::new(&command[0]);
|
183
|
+
if command.len() > 1 {
|
184
|
+
cmd.args(&command[1..]);
|
185
|
+
}
|
186
|
+
match cmd.spawn() {
|
187
|
+
Ok(mut child) => {
|
188
|
+
if let Err(e) = child.wait() {
|
189
|
+
eprintln!(
|
190
|
+
"Command {:?} failed: {:?}",
|
191
|
+
command, e
|
192
|
+
);
|
193
|
+
}
|
194
|
+
}
|
195
|
+
Err(e) => {
|
196
|
+
eprintln!(
|
197
|
+
"Failed to execute command {:?}: {:?}",
|
198
|
+
command, e
|
199
|
+
);
|
200
|
+
}
|
201
|
+
}
|
202
|
+
}
|
203
|
+
break;
|
204
|
+
}
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
Err(e) => println!("Watch error: {:?}", e),
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
// Clean up the watches.
|
214
|
+
for group in &groups {
|
215
|
+
watcher
|
216
|
+
.unwatch(&group.base_dir)
|
217
|
+
.expect("Failed to remove watch");
|
218
|
+
}
|
219
|
+
drop(watcher);
|
220
|
+
std::process::exit(0);
|
221
|
+
} else {
|
222
|
+
let _ = close(r_fd.into_raw_fd());
|
223
|
+
Ok(Some(w_fd))
|
224
|
+
}
|
225
|
+
}
|