itsi-scheduler 0.2.22-aarch64-linux

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.
Files changed (149) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/Cargo.lock +997 -0
  4. data/Cargo.toml +7 -0
  5. data/Rakefile +39 -0
  6. data/ext/itsi_acme/Cargo.toml +86 -0
  7. data/ext/itsi_acme/examples/high_level.rs +63 -0
  8. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  9. data/ext/itsi_acme/examples/low_level.rs +87 -0
  10. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  11. data/ext/itsi_acme/src/acceptor.rs +81 -0
  12. data/ext/itsi_acme/src/acme.rs +354 -0
  13. data/ext/itsi_acme/src/axum.rs +86 -0
  14. data/ext/itsi_acme/src/cache.rs +39 -0
  15. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  16. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  17. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  18. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  19. data/ext/itsi_acme/src/caches/no.rs +78 -0
  20. data/ext/itsi_acme/src/caches/test.rs +136 -0
  21. data/ext/itsi_acme/src/config.rs +172 -0
  22. data/ext/itsi_acme/src/https_helper.rs +69 -0
  23. data/ext/itsi_acme/src/incoming.rs +142 -0
  24. data/ext/itsi_acme/src/jose.rs +161 -0
  25. data/ext/itsi_acme/src/lib.rs +142 -0
  26. data/ext/itsi_acme/src/resolver.rs +59 -0
  27. data/ext/itsi_acme/src/state.rs +424 -0
  28. data/ext/itsi_error/Cargo.lock +368 -0
  29. data/ext/itsi_error/Cargo.toml +12 -0
  30. data/ext/itsi_error/src/lib.rs +140 -0
  31. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  32. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  33. data/ext/itsi_rb_helpers/Cargo.lock +355 -0
  34. data/ext/itsi_rb_helpers/Cargo.toml +11 -0
  35. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  36. data/ext/itsi_rb_helpers/src/lib.rs +232 -0
  37. data/ext/itsi_scheduler/Cargo.toml +24 -0
  38. data/ext/itsi_scheduler/extconf.rb +11 -0
  39. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  40. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  41. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler.rs +320 -0
  43. data/ext/itsi_scheduler/src/lib.rs +39 -0
  44. data/ext/itsi_server/Cargo.lock +2956 -0
  45. data/ext/itsi_server/Cargo.toml +94 -0
  46. data/ext/itsi_server/src/default_responses/mod.rs +14 -0
  47. data/ext/itsi_server/src/env.rs +43 -0
  48. data/ext/itsi_server/src/lib.rs +154 -0
  49. data/ext/itsi_server/src/prelude.rs +2 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +116 -0
  51. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +149 -0
  52. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +346 -0
  53. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +265 -0
  54. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +399 -0
  55. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +447 -0
  56. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +545 -0
  57. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +650 -0
  58. data/ext/itsi_server/src/ruby_types/itsi_server.rs +102 -0
  59. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  60. data/ext/itsi_server/src/server/binds/bind.rs +204 -0
  61. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  62. data/ext/itsi_server/src/server/binds/listener.rs +485 -0
  63. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  64. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  65. data/ext/itsi_server/src/server/binds/tls.rs +278 -0
  66. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  67. data/ext/itsi_server/src/server/frame_stream.rs +143 -0
  68. data/ext/itsi_server/src/server/http_message_types.rs +230 -0
  69. data/ext/itsi_server/src/server/io_stream.rs +128 -0
  70. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +93 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +329 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +300 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +188 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +168 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +183 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +133 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +122 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +407 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +155 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +54 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +138 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +269 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +62 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +218 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  99. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  100. data/ext/itsi_server/src/server/mod.rs +14 -0
  101. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  102. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  103. data/ext/itsi_server/src/server/request_job.rs +11 -0
  104. data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +100 -0
  105. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +411 -0
  106. data/ext/itsi_server/src/server/serve_strategy/mod.rs +31 -0
  107. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +449 -0
  108. data/ext/itsi_server/src/server/signal.rs +129 -0
  109. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  110. data/ext/itsi_server/src/server/thread_worker.rs +504 -0
  111. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  112. data/ext/itsi_server/src/services/itsi_http_service.rs +270 -0
  113. data/ext/itsi_server/src/services/mime_types.rs +2896 -0
  114. data/ext/itsi_server/src/services/mod.rs +6 -0
  115. data/ext/itsi_server/src/services/password_hasher.rs +89 -0
  116. data/ext/itsi_server/src/services/rate_limiter.rs +609 -0
  117. data/ext/itsi_server/src/services/static_file_server.rs +1400 -0
  118. data/ext/itsi_tracing/Cargo.lock +274 -0
  119. data/ext/itsi_tracing/Cargo.toml +17 -0
  120. data/ext/itsi_tracing/src/lib.rs +370 -0
  121. data/itsi-scheduler-100.png +0 -0
  122. data/lib/itsi/schedule_refinement.rb +96 -0
  123. data/lib/itsi/scheduler/3.1/itsi_scheduler.so +0 -0
  124. data/lib/itsi/scheduler/3.2/itsi_scheduler.so +0 -0
  125. data/lib/itsi/scheduler/3.3/itsi_scheduler.so +0 -0
  126. data/lib/itsi/scheduler/3.4/itsi_scheduler.so +0 -0
  127. data/lib/itsi/scheduler/4.0/itsi_scheduler.so +0 -0
  128. data/lib/itsi/scheduler/native_extension.rb +34 -0
  129. data/lib/itsi/scheduler/version.rb +7 -0
  130. data/lib/itsi/scheduler.rb +153 -0
  131. data/vendor/rb-sys-build/.cargo-ok +1 -0
  132. data/vendor/rb-sys-build/.cargo_vcs_info.json +6 -0
  133. data/vendor/rb-sys-build/Cargo.lock +294 -0
  134. data/vendor/rb-sys-build/Cargo.toml +71 -0
  135. data/vendor/rb-sys-build/Cargo.toml.orig +32 -0
  136. data/vendor/rb-sys-build/LICENSE-APACHE +190 -0
  137. data/vendor/rb-sys-build/LICENSE-MIT +21 -0
  138. data/vendor/rb-sys-build/src/bindings/sanitizer.rs +185 -0
  139. data/vendor/rb-sys-build/src/bindings/stable_api.rs +247 -0
  140. data/vendor/rb-sys-build/src/bindings/wrapper.h +71 -0
  141. data/vendor/rb-sys-build/src/bindings.rs +280 -0
  142. data/vendor/rb-sys-build/src/cc.rs +421 -0
  143. data/vendor/rb-sys-build/src/lib.rs +12 -0
  144. data/vendor/rb-sys-build/src/rb_config/flags.rs +101 -0
  145. data/vendor/rb-sys-build/src/rb_config/library.rs +132 -0
  146. data/vendor/rb-sys-build/src/rb_config/search_path.rs +57 -0
  147. data/vendor/rb-sys-build/src/rb_config.rs +906 -0
  148. data/vendor/rb-sys-build/src/utils.rs +53 -0
  149. metadata +210 -0
@@ -0,0 +1,545 @@
1
+ use derive_more::Debug;
2
+ use globset::{Glob, GlobSet, GlobSetBuilder};
3
+ use magnus::error::Result;
4
+ use nix::unistd::{close, dup, fork, pipe, read, write};
5
+ use notify::event::ModifyKind;
6
+ use notify::{Event, EventKind, RecursiveMode, Watcher};
7
+ use parking_lot::Mutex;
8
+ use std::collections::{HashMap, HashSet};
9
+ use std::fs;
10
+ use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
11
+ use std::path::Path;
12
+ use std::path::PathBuf;
13
+ use std::process::Command;
14
+
15
+ use std::sync::{mpsc, Arc};
16
+ use std::thread;
17
+ use std::time::{Duration, Instant};
18
+ use tracing::{error, info};
19
+
20
+ #[derive(Debug, Clone)]
21
+ struct PatternGroup {
22
+ base_dir: PathBuf,
23
+ glob_set: GlobSet,
24
+ commands: Vec<Vec<String>>,
25
+ pattern: String,
26
+ last_triggered: Option<Instant>,
27
+ }
28
+
29
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30
+ pub enum WatcherCommand {
31
+ Stop,
32
+ ConfigError,
33
+ Continue,
34
+ }
35
+
36
+ #[derive(Debug)]
37
+ pub struct WatcherPipes {
38
+ pub read_fd: OwnedFd,
39
+ pub write_fd: OwnedFd,
40
+ }
41
+
42
+ impl AsRawFd for WatcherPipes {
43
+ fn as_raw_fd(&self) -> RawFd {
44
+ self.read_fd.as_raw_fd()
45
+ }
46
+ }
47
+
48
+ impl Drop for WatcherPipes {
49
+ fn drop(&mut self) {
50
+ let _ = send_watcher_command(&self.write_fd, WatcherCommand::Stop);
51
+ let _ = close(self.read_fd.as_raw_fd());
52
+ let _ = close(self.write_fd.as_raw_fd());
53
+ }
54
+ }
55
+
56
+ fn extract_and_canonicalize_base_dir(pattern: &str) -> (PathBuf, String) {
57
+ let path = Path::new(pattern);
58
+ let mut base = PathBuf::new();
59
+ let mut remaining_components = Vec::new();
60
+ let mut found_glob = false;
61
+
62
+ for comp in path.components() {
63
+ let comp_str = comp.as_os_str().to_string_lossy();
64
+ if !found_glob
65
+ && (comp_str.contains('*') || comp_str.contains('?') || comp_str.contains('['))
66
+ {
67
+ found_glob = true;
68
+ remaining_components.push(comp_str.to_string());
69
+ } else if found_glob {
70
+ remaining_components.push(comp_str.to_string());
71
+ } else {
72
+ base.push(comp);
73
+ }
74
+ }
75
+
76
+ let base = if base.as_os_str().is_empty() {
77
+ PathBuf::from(".")
78
+ } else {
79
+ base
80
+ };
81
+ let base = fs::canonicalize(&base).unwrap_or(base);
82
+ let remaining_pattern = remaining_components.join("/");
83
+
84
+ (base, remaining_pattern)
85
+ }
86
+
87
+ const DEBOUNCE_DURATION: Duration = Duration::from_millis(300);
88
+ const EVENT_DEDUP_DURATION: Duration = Duration::from_millis(50);
89
+ const AUTO_RECOVERY_TIMEOUT: Duration = Duration::from_secs(5);
90
+
91
+ fn serialize_command(cmd: WatcherCommand) -> u8 {
92
+ match cmd {
93
+ WatcherCommand::Stop => 0,
94
+ WatcherCommand::ConfigError => 1,
95
+ WatcherCommand::Continue => 2,
96
+ }
97
+ }
98
+
99
+ fn deserialize_command(byte: u8) -> Option<WatcherCommand> {
100
+ match byte {
101
+ 0 => Some(WatcherCommand::Stop),
102
+ 1 => Some(WatcherCommand::ConfigError),
103
+ 2 => Some(WatcherCommand::Continue),
104
+ _ => None,
105
+ }
106
+ }
107
+
108
+ pub fn send_watcher_command(fd: &OwnedFd, cmd: WatcherCommand) -> Result<()> {
109
+ let buf = [serialize_command(cmd)];
110
+ match write(fd, &buf) {
111
+ Ok(_) => Ok(()),
112
+ Err(e) => Err(magnus::Error::new(
113
+ magnus::Ruby::get().unwrap().exception_standard_error(),
114
+ format!("Failed to send command to watcher: {}", e),
115
+ )),
116
+ }
117
+ }
118
+
119
+ pub fn watch_groups(
120
+ pattern_groups: Vec<(String, Vec<Vec<String>>)>,
121
+ ) -> Result<Option<WatcherPipes>> {
122
+ // Create bidirectional pipes for communication
123
+ let (parent_read_fd, child_write_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
124
+ magnus::Error::new(
125
+ magnus::Ruby::get().unwrap().exception_standard_error(),
126
+ format!("Failed to create parent read pipe: {}", e),
127
+ )
128
+ })?;
129
+
130
+ let (child_read_fd, parent_write_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
131
+ magnus::Error::new(
132
+ magnus::Ruby::get().unwrap().exception_standard_error(),
133
+ format!("Failed to create child read pipe: {}", e),
134
+ )
135
+ })?;
136
+
137
+ let fork_result = unsafe {
138
+ fork().map_err(|e| {
139
+ magnus::Error::new(
140
+ magnus::Ruby::get().unwrap().exception_standard_error(),
141
+ format!("Failed to fork file watcher: {}", e),
142
+ )
143
+ })
144
+ }?;
145
+
146
+ if fork_result.is_child() {
147
+ // Child process - close the parent ends of the pipes
148
+ let _ = close(parent_read_fd.into_raw_fd());
149
+ let _ = close(parent_write_fd.into_raw_fd());
150
+
151
+ let _child_read_fd_clone =
152
+ unsafe { OwnedFd::from_raw_fd(dup(child_read_fd.as_raw_fd()).unwrap()) };
153
+ let child_write_fd_clone =
154
+ unsafe { OwnedFd::from_raw_fd(dup(child_write_fd.as_raw_fd()).unwrap()) };
155
+
156
+ let command_channel = Arc::new(Mutex::new(None));
157
+ let command_channel_clone = command_channel.clone();
158
+
159
+ // Thread to read commands from parent
160
+ thread::spawn(move || {
161
+ let mut buf = [0u8; 1];
162
+ loop {
163
+ match read(child_read_fd.as_raw_fd(), &mut buf) {
164
+ Ok(0) => {
165
+ info!("Parent closed command pipe, exiting watcher");
166
+ std::process::exit(0);
167
+ }
168
+ Ok(_) => {
169
+ if let Some(cmd) = deserialize_command(buf[0]) {
170
+ info!("Received command from parent: {:?}", cmd);
171
+ *command_channel_clone.lock() = Some(cmd);
172
+
173
+ if matches!(cmd, WatcherCommand::Stop) {
174
+ info!("Received stop command, exiting watcher");
175
+ std::process::exit(0);
176
+ }
177
+ }
178
+ }
179
+ Err(e) => {
180
+ error!("Error reading from command pipe: {}", e);
181
+ std::process::exit(1);
182
+ }
183
+ }
184
+ }
185
+ });
186
+
187
+ let mut groups = Vec::new();
188
+ for (pattern, commands) in pattern_groups.into_iter() {
189
+ let (base_dir, remaining_pattern) = extract_and_canonicalize_base_dir(&pattern);
190
+ info!(
191
+ "Watching base directory {:?} with pattern {:?} (original: {:?})",
192
+ base_dir, remaining_pattern, pattern
193
+ );
194
+
195
+ let glob = Glob::new(&remaining_pattern).map_err(|e| {
196
+ magnus::Error::new(
197
+ magnus::Ruby::get().unwrap().exception_standard_error(),
198
+ format!(
199
+ "Failed to create watch glob for pattern '{}': {}",
200
+ remaining_pattern, e
201
+ ),
202
+ )
203
+ })?;
204
+ let glob_set = GlobSetBuilder::new().add(glob).build().map_err(|e| {
205
+ magnus::Error::new(
206
+ magnus::Ruby::get().unwrap().exception_standard_error(),
207
+ format!("Failed to create watch glob set: {}", e),
208
+ )
209
+ })?;
210
+
211
+ groups.push(PatternGroup {
212
+ base_dir,
213
+ glob_set,
214
+ commands,
215
+ pattern: remaining_pattern,
216
+ last_triggered: None,
217
+ });
218
+ }
219
+
220
+ // Create a channel and a watcher
221
+ let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
222
+ let startup_time = Instant::now();
223
+ let sender = tx.clone();
224
+
225
+ let event_fn = move |res: notify::Result<Event>| {
226
+ if let Ok(event) = res {
227
+ sender.send(Ok(event)).unwrap_or_else(|e| {
228
+ error!("Failed to send event: {}", e);
229
+ });
230
+ } else if let Err(e) = res {
231
+ error!("Watch error: {:?}", e);
232
+ }
233
+ };
234
+
235
+ let mut watched_paths = HashSet::new();
236
+ let mut watcher = notify::recommended_watcher(event_fn).expect("Failed to create watcher");
237
+
238
+ for group in &groups {
239
+ if watched_paths.insert(group.base_dir.clone()) {
240
+ let recursive = if group.pattern.is_empty() {
241
+ RecursiveMode::NonRecursive
242
+ } else {
243
+ RecursiveMode::Recursive
244
+ };
245
+
246
+ watcher
247
+ .watch(&group.base_dir, recursive)
248
+ .expect("Failed to add watch");
249
+ }
250
+ }
251
+
252
+ // Wait briefly to avoid initial event storm
253
+ thread::sleep(Duration::from_millis(100));
254
+
255
+ // State management
256
+ let mut recent_events: HashMap<(PathBuf, EventKind), Instant> = HashMap::new();
257
+ let restart_state = Arc::new(Mutex::new(None::<Instant>));
258
+
259
+ // Main event loop
260
+ for res in rx {
261
+ match res {
262
+ Ok(event) => {
263
+ if !matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) {
264
+ continue;
265
+ }
266
+
267
+ let now = Instant::now();
268
+
269
+ // Skip startup events
270
+ if now.duration_since(startup_time) < Duration::from_millis(500) {
271
+ continue;
272
+ }
273
+
274
+ // Deduplicate events
275
+ let mut should_process = true;
276
+ for path in &event.paths {
277
+ let event_key = (path.clone(), event.kind);
278
+ if let Some(&last_seen) = recent_events.get(&event_key) {
279
+ if now.duration_since(last_seen) < EVENT_DEDUP_DURATION {
280
+ should_process = false;
281
+ break;
282
+ }
283
+ }
284
+ recent_events.insert(event_key, now);
285
+ }
286
+
287
+ if !should_process {
288
+ continue;
289
+ }
290
+
291
+ // Clean up old entries
292
+ recent_events
293
+ .retain(|_, &mut time| now.duration_since(time) < Duration::from_secs(1));
294
+
295
+ // Check restart state
296
+ let should_skip = {
297
+ let state = restart_state.lock();
298
+ if let Some(restart_time) = *state {
299
+ now.duration_since(restart_time) < Duration::from_millis(500)
300
+ } else {
301
+ false
302
+ }
303
+ };
304
+
305
+ if should_skip {
306
+ continue;
307
+ }
308
+
309
+ // Process commands from parent
310
+ let command_to_process = {
311
+ let mut command_guard = command_channel.lock();
312
+ let cmd = *command_guard;
313
+ *command_guard = None;
314
+ cmd
315
+ };
316
+
317
+ if let Some(cmd) = command_to_process {
318
+ match cmd {
319
+ WatcherCommand::ConfigError => {
320
+ info!("Received config error notification, resuming file watching");
321
+ *restart_state.lock() = None;
322
+ for group in &mut groups {
323
+ group.last_triggered = None;
324
+ }
325
+ recent_events.clear();
326
+ }
327
+ WatcherCommand::Continue => {
328
+ info!("Received continue notification, resuming file watching");
329
+ *restart_state.lock() = None;
330
+ }
331
+ WatcherCommand::Stop => { /* Handled in command thread */ }
332
+ }
333
+ }
334
+
335
+ // Process file events
336
+ for group in &mut groups {
337
+ // Apply debounce
338
+ if let Some(last_triggered) = group.last_triggered {
339
+ if now.duration_since(last_triggered) < DEBOUNCE_DURATION {
340
+ continue;
341
+ }
342
+ }
343
+
344
+ for path in event.paths.iter() {
345
+ let matches = if group.pattern.is_empty() {
346
+ path == &group.base_dir
347
+ } else if let Ok(rel_path) = path.strip_prefix(&group.base_dir) {
348
+ group.glob_set.is_match(rel_path)
349
+ } else {
350
+ false
351
+ };
352
+
353
+ if matches {
354
+ group.last_triggered = Some(now);
355
+
356
+ // Execute commands
357
+ for command in &group.commands {
358
+ if command.is_empty() {
359
+ continue;
360
+ }
361
+
362
+ // Check for shell command or restart/reload
363
+ let is_shell_command = command.len() == 1
364
+ && (command[0].contains("&&")
365
+ || command[0].contains("||")
366
+ || command[0].contains("|")
367
+ || command[0].contains(";"));
368
+
369
+ let is_restart = command
370
+ .windows(2)
371
+ .any(|w| w[0] == "itsi" && w[1] == "restart")
372
+ || (is_shell_command
373
+ && command[0].contains("itsi restart"));
374
+
375
+ let is_reload = command
376
+ .windows(2)
377
+ .any(|w| w[0] == "itsi" && w[1] == "reload")
378
+ || (is_shell_command && command[0].contains("itsi reload"));
379
+
380
+ // Handle restart/reload
381
+ if is_restart || is_reload {
382
+ let cmd_type =
383
+ if is_restart { "restart" } else { "reload" };
384
+ let mut should_run = false;
385
+
386
+ {
387
+ let mut state = restart_state.lock();
388
+ if let Some(last_time) = *state {
389
+ if now.duration_since(last_time)
390
+ < Duration::from_secs(3)
391
+ {
392
+ info!(
393
+ "Ignoring {} command - too soon",
394
+ cmd_type
395
+ );
396
+ } else {
397
+ *state = Some(now);
398
+ should_run = true;
399
+ }
400
+ } else {
401
+ *state = Some(now);
402
+ should_run = true;
403
+ }
404
+ }
405
+
406
+ if !should_run {
407
+ continue;
408
+ }
409
+
410
+ // Notify parent (optional)
411
+ let _ = write(&child_write_fd_clone, &[3]);
412
+ }
413
+
414
+ // Build and execute command
415
+ let mut cmd = if is_shell_command {
416
+ let mut shell_cmd = Command::new("sh");
417
+ shell_cmd.arg("-c").arg(command.join(" "));
418
+ shell_cmd
419
+ } else {
420
+ let mut cmd = Command::new(&command[0]);
421
+ if command.len() > 1 {
422
+ cmd.args(&command[1..]);
423
+ }
424
+ cmd
425
+ };
426
+
427
+ match cmd.spawn() {
428
+ Ok(mut child) => {
429
+ if let Err(e) = child.wait() {
430
+ error!("Command {:?} failed: {:?}", command, e);
431
+ }
432
+
433
+ if is_restart || is_reload {
434
+ info!("Itsi command submitted, waiting for parent response");
435
+
436
+ // Set auto-recovery timer
437
+ let restart_state_clone =
438
+ Arc::clone(&restart_state);
439
+ let now_clone = now;
440
+ thread::spawn(move || {
441
+ thread::sleep(AUTO_RECOVERY_TIMEOUT);
442
+ let mut state = restart_state_clone.lock();
443
+ if let Some(restart_time) = *state {
444
+ if now_clone.duration_since(restart_time)
445
+ < Duration::from_secs(1)
446
+ {
447
+ info!("Auto-recovering from potential restart failure");
448
+ *state = None;
449
+ }
450
+ }
451
+ });
452
+ }
453
+ }
454
+ Err(e) => {
455
+ error!(
456
+ "Failed to execute command {:?}: {:?}",
457
+ command, e
458
+ );
459
+ }
460
+ }
461
+ }
462
+ break;
463
+ }
464
+ }
465
+ }
466
+ }
467
+ Err(e) => error!("Watch error: {:?}", e),
468
+ }
469
+ }
470
+
471
+ // Clean up
472
+ drop(watcher);
473
+ std::process::exit(0);
474
+ } else {
475
+ // Parent process - close the child ends of the pipes
476
+ let _ = close(child_read_fd.into_raw_fd());
477
+ let _ = close(child_write_fd.into_raw_fd());
478
+
479
+ // Create a paired structure to return
480
+ let watcher_pipes = WatcherPipes {
481
+ read_fd: parent_read_fd,
482
+ write_fd: parent_write_fd,
483
+ };
484
+
485
+ Ok(Some(watcher_pipes))
486
+ }
487
+ }
488
+
489
+ #[cfg(test)]
490
+ mod tests {
491
+ use std::env;
492
+
493
+ use super::*;
494
+
495
+ #[test]
496
+ fn test_extract_patterns() {
497
+ // Save current dir to restore later
498
+ let original_dir = env::current_dir().unwrap();
499
+
500
+ // Create a temp dir and work from there for consistent results
501
+ let temp_dir = env::temp_dir().join("itsi_test_patterns");
502
+ let _ = fs::create_dir_all(&temp_dir);
503
+ env::set_current_dir(&temp_dir).unwrap();
504
+
505
+ // Test glob patterns
506
+ let (base, pattern) = extract_and_canonicalize_base_dir("assets/*/**.tsx");
507
+ assert!(base.ends_with("assets"));
508
+ assert_eq!(pattern, "*/**.tsx");
509
+
510
+ let (base, pattern) = extract_and_canonicalize_base_dir("./assets/*/**.tsx");
511
+ assert!(base.ends_with("assets"));
512
+ assert_eq!(pattern, "*/**.tsx");
513
+
514
+ // Test non-glob patterns - exact files should have empty pattern
515
+ let (base, pattern) = extract_and_canonicalize_base_dir("foo/bar.txt");
516
+ assert!(base.ends_with("bar.txt"));
517
+ assert_eq!(pattern, "");
518
+
519
+ // Test current directory patterns
520
+ let (base, pattern) = extract_and_canonicalize_base_dir("*.txt");
521
+ assert_eq!(base, temp_dir.canonicalize().unwrap());
522
+ assert_eq!(pattern, "*.txt");
523
+
524
+ // Test file in current directory
525
+ let (base, pattern) = extract_and_canonicalize_base_dir("test.txt");
526
+ assert!(base.ends_with("test.txt"));
527
+ assert_eq!(pattern, "");
528
+
529
+ // Restore original directory and clean up
530
+ env::set_current_dir(original_dir).unwrap();
531
+ let _ = fs::remove_dir_all(&temp_dir);
532
+ }
533
+
534
+ #[test]
535
+ fn test_watcher_commands() {
536
+ assert_eq!(serialize_command(WatcherCommand::Stop), 0);
537
+ assert_eq!(serialize_command(WatcherCommand::ConfigError), 1);
538
+ assert_eq!(serialize_command(WatcherCommand::Continue), 2);
539
+
540
+ assert_eq!(deserialize_command(0), Some(WatcherCommand::Stop));
541
+ assert_eq!(deserialize_command(1), Some(WatcherCommand::ConfigError));
542
+ assert_eq!(deserialize_command(2), Some(WatcherCommand::Continue));
543
+ assert_eq!(deserialize_command(99), None);
544
+ }
545
+ }