itsi-scheduler 0.2.16 → 0.2.18
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 +1 -1
- data/ext/itsi_acme/Cargo.toml +1 -1
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +3 -1
- data/ext/itsi_server/src/lib.rs +6 -1
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +4 -4
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +14 -13
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +64 -33
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +151 -152
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +422 -110
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +62 -15
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +1 -1
- data/ext/itsi_server/src/server/binds/listener.rs +45 -7
- data/ext/itsi_server/src/server/frame_stream.rs +142 -0
- data/ext/itsi_server/src/server/http_message_types.rs +142 -9
- data/ext/itsi_server/src/server/io_stream.rs +28 -5
- data/ext/itsi_server/src/server/lifecycle_event.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +8 -10
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +3 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +54 -56
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +5 -7
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +5 -5
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +7 -10
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +1 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +4 -6
- data/ext/itsi_server/src/server/mod.rs +1 -0
- data/ext/itsi_server/src/server/process_worker.rs +3 -4
- data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +16 -12
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +83 -31
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +166 -142
- data/ext/itsi_server/src/server/signal.rs +37 -9
- data/ext/itsi_server/src/server/thread_worker.rs +84 -69
- data/ext/itsi_server/src/services/itsi_http_service.rs +43 -43
- data/ext/itsi_server/src/services/static_file_server.rs +28 -47
- data/lib/itsi/scheduler/version.rb +1 -1
- metadata +2 -1
@@ -1,80 +1,136 @@
|
|
1
1
|
use derive_more::Debug;
|
2
2
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
3
3
|
use magnus::error::Result;
|
4
|
-
use nix::unistd::{close, fork, pipe, read};
|
4
|
+
use nix::unistd::{close, dup, fork, pipe, read, write};
|
5
5
|
use notify::event::ModifyKind;
|
6
|
-
use notify::{Event, RecursiveMode, Watcher};
|
7
|
-
use
|
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};
|
8
11
|
use std::path::Path;
|
9
|
-
use std::
|
12
|
+
use std::path::PathBuf;
|
13
|
+
use std::process::Command;
|
14
|
+
|
15
|
+
use std::sync::{mpsc, Arc};
|
16
|
+
use std::thread;
|
10
17
|
use std::time::{Duration, Instant};
|
11
|
-
use
|
12
|
-
|
13
|
-
os::fd::{AsRawFd, IntoRawFd, OwnedFd},
|
14
|
-
path::PathBuf,
|
15
|
-
process::Command,
|
16
|
-
sync::mpsc,
|
17
|
-
thread::{self},
|
18
|
-
};
|
19
|
-
use tracing::debug;
|
20
|
-
|
21
|
-
/// Represents a set of patterns and commands.
|
18
|
+
use tracing::{error, info};
|
19
|
+
|
22
20
|
#[derive(Debug, Clone)]
|
23
21
|
struct PatternGroup {
|
24
22
|
base_dir: PathBuf,
|
25
23
|
glob_set: GlobSet,
|
26
|
-
pattern: String,
|
27
24
|
commands: Vec<Vec<String>>,
|
25
|
+
pattern: String,
|
28
26
|
last_triggered: Option<Instant>,
|
29
27
|
}
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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()
|
47
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
|
+
}
|
48
55
|
|
56
|
+
fn extract_and_canonicalize_base_dir(pattern: &str) -> (PathBuf, String) {
|
49
57
|
let path = Path::new(pattern);
|
50
58
|
let mut base = PathBuf::new();
|
59
|
+
let mut remaining_components = Vec::new();
|
60
|
+
let mut found_glob = false;
|
61
|
+
|
51
62
|
for comp in path.components() {
|
52
63
|
let comp_str = comp.as_os_str().to_string_lossy();
|
53
|
-
if
|
54
|
-
|
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());
|
55
71
|
} else {
|
56
72
|
base.push(comp);
|
57
73
|
}
|
58
74
|
}
|
59
|
-
|
60
|
-
let base = if base.as_os_str().is_empty()
|
75
|
+
|
76
|
+
let base = if base.as_os_str().is_empty() {
|
61
77
|
PathBuf::from(".")
|
62
78
|
} else {
|
63
79
|
base
|
64
80
|
};
|
81
|
+
let base = fs::canonicalize(&base).unwrap_or(base);
|
82
|
+
let remaining_pattern = remaining_components.join("/");
|
83
|
+
|
84
|
+
(base, remaining_pattern)
|
85
|
+
}
|
65
86
|
|
66
|
-
|
67
|
-
|
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
|
+
}
|
68
97
|
}
|
69
98
|
|
70
|
-
|
71
|
-
|
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
|
+
}
|
72
107
|
|
73
|
-
pub fn
|
74
|
-
let
|
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::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| {
|
75
124
|
magnus::Error::new(
|
76
125
|
magnus::exception::standard_error(),
|
77
|
-
format!("Failed to create
|
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::exception::standard_error(),
|
133
|
+
format!("Failed to create child read pipe: {}", e),
|
78
134
|
)
|
79
135
|
})?;
|
80
136
|
|
@@ -88,17 +144,41 @@ pub fn watch_groups(pattern_groups: Vec<(String, Vec<Vec<String>>)>) -> Result<O
|
|
88
144
|
}?;
|
89
145
|
|
90
146
|
if fork_result.is_child() {
|
91
|
-
|
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
|
92
160
|
thread::spawn(move || {
|
93
161
|
let mut buf = [0u8; 1];
|
94
162
|
loop {
|
95
|
-
match read(
|
163
|
+
match read(child_read_fd.as_raw_fd(), &mut buf) {
|
96
164
|
Ok(0) => {
|
165
|
+
info!("Parent closed command pipe, exiting watcher");
|
97
166
|
std::process::exit(0);
|
98
167
|
}
|
99
|
-
Ok(_) => {
|
100
|
-
|
101
|
-
|
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);
|
102
182
|
}
|
103
183
|
}
|
104
184
|
}
|
@@ -106,11 +186,19 @@ pub fn watch_groups(pattern_groups: Vec<(String, Vec<Vec<String>>)>) -> Result<O
|
|
106
186
|
|
107
187
|
let mut groups = Vec::new();
|
108
188
|
for (pattern, commands) in pattern_groups.into_iter() {
|
109
|
-
let base_dir = extract_and_canonicalize_base_dir(&pattern);
|
110
|
-
|
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| {
|
111
196
|
magnus::Error::new(
|
112
197
|
magnus::exception::standard_error(),
|
113
|
-
format!(
|
198
|
+
format!(
|
199
|
+
"Failed to create watch glob for pattern '{}': {}",
|
200
|
+
remaining_pattern, e
|
201
|
+
),
|
114
202
|
)
|
115
203
|
})?;
|
116
204
|
let glob_set = GlobSetBuilder::new().add(glob).build().map_err(|e| {
|
@@ -119,115 +207,339 @@ pub fn watch_groups(pattern_groups: Vec<(String, Vec<Vec<String>>)>) -> Result<O
|
|
119
207
|
format!("Failed to create watch glob set: {}", e),
|
120
208
|
)
|
121
209
|
})?;
|
210
|
+
|
122
211
|
groups.push(PatternGroup {
|
123
212
|
base_dir,
|
124
213
|
glob_set,
|
125
|
-
pattern,
|
126
214
|
commands,
|
215
|
+
pattern: remaining_pattern,
|
127
216
|
last_triggered: None,
|
128
217
|
});
|
129
218
|
}
|
130
219
|
|
131
|
-
// Create a channel and a watcher
|
220
|
+
// Create a channel and a watcher
|
132
221
|
let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
|
222
|
+
let startup_time = Instant::now();
|
133
223
|
let sender = tx.clone();
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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);
|
140
232
|
}
|
141
|
-
}
|
233
|
+
};
|
234
|
+
|
235
|
+
let mut watched_paths = HashSet::new();
|
236
|
+
let mut watcher = notify::recommended_watcher(event_fn).expect("Failed to create watcher");
|
142
237
|
|
143
|
-
let mut watched_dirs = HashSet::new();
|
144
|
-
let mut watcher: RecommendedWatcher =
|
145
|
-
notify::recommended_watcher(event_fn(sender)).expect("Failed to create watcher");
|
146
238
|
for group in &groups {
|
147
|
-
if
|
148
|
-
|
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
|
+
|
149
246
|
watcher
|
150
|
-
.watch(&group.base_dir,
|
247
|
+
.watch(&group.base_dir, recursive)
|
151
248
|
.expect("Failed to add watch");
|
152
249
|
}
|
153
250
|
}
|
154
251
|
|
155
|
-
//
|
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
|
156
260
|
for res in rx {
|
157
261
|
match res {
|
158
262
|
Ok(event) => {
|
159
263
|
if !matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) {
|
160
264
|
continue;
|
161
265
|
}
|
266
|
+
|
162
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
|
163
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
|
+
|
164
344
|
for path in event.paths.iter() {
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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;
|
176
360
|
}
|
177
361
|
|
178
|
-
//
|
179
|
-
|
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(";"));
|
180
368
|
|
181
|
-
|
182
|
-
|
183
|
-
|
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 {
|
184
407
|
continue;
|
185
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 {
|
186
420
|
let mut cmd = Command::new(&command[0]);
|
187
421
|
if command.len() > 1 {
|
188
422
|
cmd.args(&command[1..]);
|
189
423
|
}
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
eprintln!(
|
198
|
-
"Command {:?} failed: {:?}",
|
199
|
-
command, e
|
200
|
-
);
|
201
|
-
}
|
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);
|
202
431
|
}
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
+
});
|
208
452
|
}
|
209
453
|
}
|
454
|
+
Err(e) => {
|
455
|
+
error!(
|
456
|
+
"Failed to execute command {:?}: {:?}",
|
457
|
+
command, e
|
458
|
+
);
|
459
|
+
}
|
210
460
|
}
|
211
|
-
break;
|
212
461
|
}
|
462
|
+
break;
|
213
463
|
}
|
214
464
|
}
|
215
465
|
}
|
216
466
|
}
|
217
|
-
Err(e) =>
|
467
|
+
Err(e) => error!("Watch error: {:?}", e),
|
218
468
|
}
|
219
469
|
}
|
220
470
|
|
221
|
-
// Clean up
|
222
|
-
for group in &groups {
|
223
|
-
watcher
|
224
|
-
.unwatch(&group.base_dir)
|
225
|
-
.expect("Failed to remove watch");
|
226
|
-
}
|
471
|
+
// Clean up
|
227
472
|
drop(watcher);
|
228
473
|
std::process::exit(0);
|
229
474
|
} else {
|
230
|
-
|
231
|
-
|
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);
|
232
544
|
}
|
233
545
|
}
|