zinit_client/rhai/
engine.rs

1//! Rhai engine for zinit client
2//!
3//! This module provides the Rhai scripting engine with zinit functions.
4//! The engine uses ZinitHandle (sync) to communicate with the RPC thread.
5
6use crate::client::handle::ZinitHandle;
7use rhai::{AST, Dynamic, Engine, EvalAltResult, Map, Position, Scope};
8use std::path::Path;
9use std::sync::Mutex;
10use std::time::Duration;
11
12use super::service_builder::{ServiceBuilder, XinitBuilder, rhai_fns, xinit_rhai_fns};
13
14// Import herolib Rhai registration functions
15use herolib_clients::rhai::register_redisclient_module;
16use herolib_core::rhai::register_text_module;
17use herolib_os::rhai::{
18    register_git_module, register_net_module, register_os_module, register_process_module,
19};
20
21// Global output buffer for print/debug output
22lazy_static::lazy_static! {
23    static ref OUTPUT_BUFFER: Mutex<Vec<String>> = Mutex::new(Vec::new());
24    static ref GLOBAL_HANDLE: Mutex<Option<ZinitHandle>> = Mutex::new(None);
25}
26
27/// Set the global handle for use by chained builder methods
28fn set_global_handle(handle: ZinitHandle) {
29    if let Ok(mut h) = GLOBAL_HANDLE.lock() {
30        *h = Some(handle);
31    }
32}
33
34/// Get a clone of the global handle
35pub fn get_global_handle() -> Option<ZinitHandle> {
36    GLOBAL_HANDLE.lock().ok().and_then(|h| h.clone())
37}
38
39/// Append to the output buffer
40fn append_output(s: &str) {
41    if let Ok(mut buf) = OUTPUT_BUFFER.lock() {
42        buf.push(s.to_string());
43    }
44    // Also print to stdout for immediate feedback
45    println!("{}", s);
46}
47
48/// Clear the output buffer
49pub fn clear_output() {
50    if let Ok(mut buf) = OUTPUT_BUFFER.lock() {
51        buf.clear();
52    }
53}
54
55/// Get accumulated output
56pub fn get_output() -> Vec<String> {
57    OUTPUT_BUFFER.lock().map(|b| b.clone()).unwrap_or_default()
58}
59
60/// Normalize a service name: replace - with _, lowercase
61pub fn normalize_service_name(name: &str) -> String {
62    name.replace('-', "_").to_lowercase()
63}
64
65/// Create a Rhai runtime error
66fn make_error<T>(msg: impl Into<String>) -> Result<T, Box<EvalAltResult>> {
67    Err(Box::new(EvalAltResult::ErrorRuntime(
68        Dynamic::from(msg.into()),
69        Position::NONE,
70    )))
71}
72
73/// Check if a TCP port is open
74fn check_tcp_port(addr: &str) -> bool {
75    if addr.is_empty() {
76        return true;
77    }
78    use std::net::{TcpStream, ToSocketAddrs};
79    match addr.to_socket_addrs() {
80        Ok(mut addrs) => {
81            if let Some(socket_addr) = addrs.next() {
82                TcpStream::connect_timeout(&socket_addr, Duration::from_secs(2)).is_ok()
83            } else {
84                false
85            }
86        }
87        Err(_) => false,
88    }
89}
90
91/// Check if an HTTP endpoint is reachable
92fn check_http(url: &str) -> bool {
93    if url.is_empty() {
94        return true;
95    }
96    use std::io::{Read, Write};
97    use std::net::{TcpStream, ToSocketAddrs};
98
99    let url = url.trim();
100    let without_proto = url
101        .strip_prefix("http://")
102        .or_else(|| url.strip_prefix("https://"))
103        .unwrap_or(url);
104
105    let (host_port, path) = match without_proto.find('/') {
106        Some(i) => (&without_proto[..i], &without_proto[i..]),
107        None => (without_proto, "/"),
108    };
109
110    let host_port_with_default = if host_port.contains(':') {
111        host_port.to_string()
112    } else {
113        format!("{}:80", host_port)
114    };
115
116    let socket_addr = match host_port_with_default.to_socket_addrs() {
117        Ok(mut addrs) => match addrs.next() {
118            Some(a) => a,
119            None => return false,
120        },
121        Err(_) => return false,
122    };
123
124    match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(2)) {
125        Ok(mut stream) => {
126            stream.set_read_timeout(Some(Duration::from_secs(2))).ok();
127            stream.set_write_timeout(Some(Duration::from_secs(2))).ok();
128
129            let request = format!(
130                "GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n",
131                path, host_port
132            );
133
134            if stream.write_all(request.as_bytes()).is_err() {
135                return false;
136            }
137
138            let mut response = [0u8; 32];
139            if stream.read(&mut response).is_err() {
140                return false;
141            }
142
143            let response_str = String::from_utf8_lossy(&response);
144            response_str.contains("HTTP/1.0 2") || response_str.contains("HTTP/1.1 2")
145        }
146        Err(_) => false,
147    }
148}
149
150/// Get child PIDs of a process
151fn get_child_pids(parent_pid: i32) -> Vec<i32> {
152    use std::process::Command;
153
154    let output = Command::new("pgrep")
155        .args(["-P", &parent_pid.to_string()])
156        .output();
157
158    match output {
159        Ok(out) if out.status.success() => std::str::from_utf8(&out.stdout)
160            .unwrap_or("")
161            .lines()
162            .filter_map(|s| s.trim().parse().ok())
163            .collect(),
164        _ => Vec::new(),
165    }
166}
167
168/// Recursively collect all descendant PIDs (depth-first, deepest first)
169fn collect_descendants_depth_first(pid: i32, result: &mut Vec<i32>) {
170    let children = get_child_pids(pid);
171    for child in children {
172        collect_descendants_depth_first(child, result);
173        result.push(child);
174    }
175}
176
177/// Kill a process and all its children (children first, then parent)
178fn kill_process_tree(pid: i32) {
179    use std::process::Command;
180
181    // Collect all descendants depth-first (deepest first)
182    let mut descendants = Vec::new();
183    collect_descendants_depth_first(pid, &mut descendants);
184
185    // Kill children first (deepest first), then parent
186    for child_pid in &descendants {
187        let _ = Command::new("kill").arg(child_pid.to_string()).output();
188    }
189    let _ = Command::new("kill").arg(pid.to_string()).output();
190
191    // Wait briefly
192    std::thread::sleep(Duration::from_millis(100));
193
194    // Force kill any remaining
195    for child_pid in &descendants {
196        let check = Command::new("kill")
197            .args(["-0", &child_pid.to_string()])
198            .output();
199        if check.map(|o| o.status.success()).unwrap_or(false) {
200            let _ = Command::new("kill")
201                .args(["-9", &child_pid.to_string()])
202                .output();
203        }
204    }
205    let check = Command::new("kill").args(["-0", &pid.to_string()]).output();
206    if check.map(|o| o.status.success()).unwrap_or(false) {
207        let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
208    }
209}
210
211/// Kill process(es) listening on a TCP port (used by kill_port rhai function)
212fn kill_process_on_port(port: u16) -> usize {
213    use std::process::Command;
214
215    let output = Command::new("lsof")
216        .args(["-ti", &format!(":{}", port)])
217        .output();
218
219    match output {
220        Ok(out) if out.status.success() => {
221            let pids: Vec<i32> = std::str::from_utf8(&out.stdout)
222                .unwrap_or("")
223                .lines()
224                .filter_map(|s| s.trim().parse().ok())
225                .collect();
226
227            let mut killed = 0;
228            for pid in &pids {
229                kill_process_tree(*pid);
230                killed += 1;
231            }
232            killed
233        }
234        _ => 0,
235    }
236}
237
238/// Zinit Rhai scripting engine
239pub struct ZinitEngine {
240    engine: Engine,
241}
242
243impl ZinitEngine {
244    /// Create a new engine without server connection (utility functions only)
245    pub fn new() -> Self {
246        let mut engine = Engine::new();
247        Self::register_types(&mut engine);
248        Self::register_utility_functions(&mut engine);
249        Self { engine }
250    }
251
252    /// Create a new engine with ZinitHandle for server communication
253    pub fn with_handle(handle: ZinitHandle) -> Self {
254        // Store handle globally for chained builder methods
255        set_global_handle(handle.clone());
256
257        let mut engine = Engine::new();
258        Self::register_types(&mut engine);
259        Self::register_utility_functions(&mut engine);
260        Self::register_zinit_functions(&mut engine, handle);
261        Self { engine }
262    }
263
264    fn register_types(engine: &mut Engine) {
265        engine
266            .register_type_with_name::<ServiceBuilder>("ServiceBuilder")
267            .register_fn("zinit_service_new", rhai_fns::new_service)
268            .register_fn("exec", rhai_fns::service_exec)
269            .register_fn("status", rhai_fns::service_status)
270            .register_fn("period", rhai_fns::service_period)
271            .register_fn("retry", rhai_fns::service_retry)
272            .register_fn("test_cmd", rhai_fns::service_test_cmd)
273            .register_fn("oneshot", rhai_fns::service_oneshot)
274            .register_fn("shutdown_timeout", rhai_fns::service_shutdown_timeout)
275            .register_fn("after", rhai_fns::service_after)
276            .register_fn("signal_stop", rhai_fns::service_signal_stop)
277            .register_fn("log", rhai_fns::service_log)
278            .register_fn("env", rhai_fns::service_env)
279            .register_fn("dir", rhai_fns::service_dir)
280            .register_fn("to_map", rhai_fns::service_to_map)
281            .register_fn("to_string", rhai_fns::service_to_string)
282            .register_fn("to_toml", rhai_fns::service_to_toml)
283            .register_fn("to_markdown", rhai_fns::service_to_markdown)
284            .register_fn("name", rhai_fns::service_name)
285            .register_fn("test_tcp", rhai_fns::service_test_tcp)
286            .register_fn("test_http", rhai_fns::service_test_http)
287            .register_fn("tcp_kill", rhai_fns::service_tcp_kill)
288            .register_fn("reset", rhai_fns::service_reset)
289            .register_fn("wait", rhai_fns::service_wait)
290            .register_fn("register", rhai_fns::service_register);
291
292        // Register XinitBuilder type and methods
293        engine
294            .register_type_with_name::<XinitBuilder>("XinitBuilder")
295            .register_fn("xinit_proxy_new", xinit_rhai_fns::new_xinit)
296            .register_fn("name", xinit_rhai_fns::xinit_name)
297            .register_fn("listen", xinit_rhai_fns::xinit_listen)
298            .register_fn("add_listen", xinit_rhai_fns::xinit_add_listen)
299            .register_fn("backend", xinit_rhai_fns::xinit_backend)
300            .register_fn("service", xinit_rhai_fns::xinit_service)
301            .register_fn("reset", xinit_rhai_fns::xinit_reset)
302            .register_fn("idle_timeout", xinit_rhai_fns::xinit_idle_timeout)
303            .register_fn("connect_timeout", xinit_rhai_fns::xinit_connect_timeout)
304            .register_fn("to_string", xinit_rhai_fns::xinit_to_string)
305            .register_fn("to_toml", xinit_rhai_fns::xinit_to_toml)
306            .register_fn("to_markdown", xinit_rhai_fns::xinit_to_markdown)
307            .register_fn("register", xinit_rhai_fns::xinit_register);
308    }
309
310    fn register_utility_functions(engine: &mut Engine) {
311        // Capture print/debug output
312        engine.on_print(|s| {
313            append_output(s);
314        });
315        engine.on_debug(|s, source, pos| {
316            let location = if let Some(src) = source {
317                format!("[{}:{}] ", src, pos)
318            } else if !pos.is_none() {
319                format!("[{}] ", pos)
320            } else {
321                String::new()
322            };
323            append_output(&format!("[DEBUG] {}{}", location, s));
324        });
325
326        engine.register_fn("sleep_ms", |ms: i64| {
327            std::thread::sleep(Duration::from_millis(ms as u64));
328        });
329
330        engine.register_fn("sleep", |secs: i64| {
331            std::thread::sleep(Duration::from_secs(secs as u64));
332        });
333
334        engine.register_fn("get_env", |key: &str| -> Dynamic {
335            match std::env::var(key) {
336                Ok(val) => Dynamic::from(val),
337                Err(_) => Dynamic::UNIT,
338            }
339        });
340
341        engine.register_fn("set_env", |key: &str, value: &str| {
342            // SAFETY: We are single-threaded in the Rhai engine context
343            unsafe { std::env::set_var(key, value) };
344        });
345
346        engine.register_fn("check_tcp", check_tcp_port);
347        engine.register_fn("check_http", check_http);
348
349        engine.register_fn("kill_port", |port: i64| -> i64 {
350            kill_process_on_port(port as u16) as i64
351        });
352
353        // Register herolib modules (OS, process, git, network)
354        let _ = register_os_module(engine);
355        let _ = register_process_module(engine);
356        let _ = register_git_module(engine);
357        let _ = register_net_module(engine);
358
359        // Register herolib-core text module
360        let _ = register_text_module(engine);
361
362        // Register herolib-clients redis module
363        let _ = register_redisclient_module(engine);
364    }
365
366    fn register_zinit_functions(engine: &mut Engine, handle: ZinitHandle) {
367        // zinit_ping() -> map
368        let h = handle.clone();
369        engine.register_fn(
370            "zinit_ping",
371            move || -> Result<Dynamic, Box<EvalAltResult>> {
372                match h.ping() {
373                    Ok(resp) => {
374                        let mut map = Map::new();
375                        map.insert("message".into(), resp.message.into());
376                        map.insert("version".into(), resp.version.into());
377                        Ok(Dynamic::from_map(map))
378                    }
379                    Err(e) => make_error(format!("zinit_ping failed: {}", e)),
380                }
381            },
382        );
383
384        // zinit_list() -> array
385        let h = handle.clone();
386        engine.register_fn(
387            "zinit_list",
388            move || -> Result<Dynamic, Box<EvalAltResult>> {
389                match h.list() {
390                    Ok(services) => Ok(Dynamic::from_array(
391                        services.into_iter().map(Dynamic::from).collect(),
392                    )),
393                    Err(e) => make_error(format!("zinit_list failed: {}", e)),
394                }
395            },
396        );
397
398        // zinit_list_md() -> prints markdown table of all services
399        let h = handle.clone();
400        engine.register_fn(
401            "zinit_list_md",
402            move || -> Result<(), Box<EvalAltResult>> {
403                match h.list() {
404                    Ok(services) => {
405                        if services.is_empty() {
406                            append_output("No services registered.");
407                        } else {
408                            let mut output = String::new();
409                            output.push_str("| Service | State | PID |\n");
410                            output.push_str("|---------|-------|-----|\n");
411                            for name in services {
412                                match h.status(&name) {
413                                    Ok(status) => {
414                                        output.push_str(&format!(
415                                            "| {} | {} | {} |\n",
416                                            status.name, status.state, status.pid
417                                        ));
418                                    }
419                                    Err(_) => {
420                                        output.push_str(&format!("| {} | unknown | - |\n", name));
421                                    }
422                                }
423                            }
424                            append_output(&output);
425                        }
426                        Ok(())
427                    }
428                    Err(e) => make_error(format!("zinit_list_md failed: {}", e)),
429                }
430            },
431        );
432
433        // zinit_status(name) -> map
434        let h = handle.clone();
435        engine.register_fn(
436            "zinit_status",
437            move |name: &str| -> Result<Dynamic, Box<EvalAltResult>> {
438                let name = normalize_service_name(name);
439                match h.status(&name) {
440                    Ok(status) => {
441                        let mut map = Map::new();
442                        map.insert("name".into(), status.name.into());
443                        map.insert("pid".into(), (status.pid as i64).into());
444                        map.insert("state".into(), status.state.into());
445                        map.insert("target".into(), status.target.into());
446                        Ok(Dynamic::from_map(map))
447                    }
448                    Err(e) => make_error(format!("zinit_status '{}' failed: {}", name, e)),
449                }
450            },
451        );
452
453        // zinit_status_md(name) -> prints markdown table of service status
454        let h = handle.clone();
455        engine.register_fn(
456            "zinit_status_md",
457            move |name: &str| -> Result<(), Box<EvalAltResult>> {
458                let name = normalize_service_name(name);
459                match h.status(&name) {
460                    Ok(status) => {
461                        let mut output = String::new();
462                        output.push_str("| Property | Value |\n");
463                        output.push_str("|----------|-------|\n");
464                        output.push_str(&format!("| Name | {} |\n", status.name));
465                        output.push_str(&format!("| PID | {} |\n", status.pid));
466                        output.push_str(&format!("| State | {} |\n", status.state));
467                        output.push_str(&format!("| Target | {} |\n", status.target));
468                        append_output(&output);
469                        Ok(())
470                    }
471                    Err(e) => make_error(format!("zinit_status_md '{}' failed: {}", name, e)),
472                }
473            },
474        );
475
476        // zinit_start(name)
477        let h = handle.clone();
478        engine.register_fn(
479            "zinit_start",
480            move |name: &str| -> Result<(), Box<EvalAltResult>> {
481                let name = normalize_service_name(name);
482                match h.start(&name) {
483                    Ok(_) => {
484                        append_output(&format!("Started: {}", name));
485                        Ok(())
486                    }
487                    Err(e) => make_error(format!("zinit_start '{}' failed: {}", name, e)),
488                }
489            },
490        );
491
492        // zinit_stop(name)
493        let h = handle.clone();
494        engine.register_fn(
495            "zinit_stop",
496            move |name: &str| -> Result<(), Box<EvalAltResult>> {
497                let name = normalize_service_name(name);
498                match h.stop(&name) {
499                    Ok(_) => {
500                        append_output(&format!("Stopped: {}", name));
501                        Ok(())
502                    }
503                    Err(e) => make_error(format!("zinit_stop '{}' failed: {}", name, e)),
504                }
505            },
506        );
507
508        // zinit_restart(name)
509        let h = handle.clone();
510        engine.register_fn(
511            "zinit_restart",
512            move |name: &str| -> Result<(), Box<EvalAltResult>> {
513                let name = normalize_service_name(name);
514                match h.restart(&name) {
515                    Ok(_) => {
516                        append_output(&format!("Restarted: {}", name));
517                        Ok(())
518                    }
519                    Err(e) => make_error(format!("zinit_restart '{}' failed: {}", name, e)),
520                }
521            },
522        );
523
524        // zinit_delete(name)
525        let h = handle.clone();
526        engine.register_fn(
527            "zinit_delete",
528            move |name: &str| -> Result<(), Box<EvalAltResult>> {
529                let name = normalize_service_name(name);
530                match h.delete(&name) {
531                    Ok(_) => {
532                        append_output(&format!("Deleted: {}", name));
533                        Ok(())
534                    }
535                    Err(e) => make_error(format!("zinit_delete '{}' failed: {}", name, e)),
536                }
537            },
538        );
539
540        // zinit_kill(name, signal)
541        let h = handle.clone();
542        engine.register_fn(
543            "zinit_kill",
544            move |name: &str, signal: &str| -> Result<(), Box<EvalAltResult>> {
545                let name = normalize_service_name(name);
546                match h.kill(&name, signal) {
547                    Ok(_) => {
548                        append_output(&format!("Sent {} to: {}", signal, name));
549                        Ok(())
550                    }
551                    Err(e) => make_error(format!("zinit_kill '{}' failed: {}", name, e)),
552                }
553            },
554        );
555
556        // zinit_stats(name) -> map
557        let h = handle.clone();
558        engine.register_fn(
559            "zinit_stats",
560            move |name: &str| -> Result<Dynamic, Box<EvalAltResult>> {
561                let name = normalize_service_name(name);
562                match h.stats(&name) {
563                    Ok(stats) => {
564                        let mut map = Map::new();
565                        map.insert("pid".into(), (stats.pid as i64).into());
566                        map.insert("memory_usage".into(), (stats.memory_usage as i64).into());
567                        map.insert("cpu_usage".into(), (stats.cpu_usage as f64).into());
568                        Ok(Dynamic::from_map(map))
569                    }
570                    Err(e) => make_error(format!("zinit_stats '{}' failed: {}", name, e)),
571                }
572            },
573        );
574
575        // zinit_stats_md(name) -> prints markdown table of service stats
576        let h = handle.clone();
577        engine.register_fn(
578            "zinit_stats_md",
579            move |name: &str| -> Result<(), Box<EvalAltResult>> {
580                let name = normalize_service_name(name);
581                match h.stats(&name) {
582                    Ok(stats) => {
583                        let mut output = String::new();
584                        output.push_str("| Metric | Value |\n");
585                        output.push_str("|--------|-------|\n");
586                        output.push_str(&format!("| PID | {} |\n", stats.pid));
587                        output.push_str(&format!("| Memory | {} bytes |\n", stats.memory_usage));
588                        output.push_str(&format!("| CPU | {:.2}% |\n", stats.cpu_usage));
589                        append_output(&output);
590                        Ok(())
591                    }
592                    Err(e) => make_error(format!("zinit_stats_md '{}' failed: {}", name, e)),
593                }
594            },
595        );
596
597        // zinit_is_running(name) -> bool
598        let h = handle.clone();
599        engine.register_fn("zinit_is_running", move |name: &str| -> bool {
600            let name = normalize_service_name(name);
601            h.is_running(&name).unwrap_or(false)
602        });
603
604        // zinit_wait_for(name, timeout_secs) -> bool
605        let h = handle.clone();
606        engine.register_fn("zinit_wait_for", move |name: &str, secs: i64| -> bool {
607            let name = normalize_service_name(name);
608            let deadline = std::time::Instant::now() + Duration::from_secs(secs.max(1) as u64);
609            loop {
610                if std::time::Instant::now() >= deadline {
611                    return false;
612                }
613                if let Ok(status) = h.status(&name) {
614                    if status.state == "Running" || status.state == "Success" {
615                        return true;
616                    }
617                    if status.state.contains("Error") || status.state.contains("Failed") {
618                        return false;
619                    }
620                }
621                std::thread::sleep(Duration::from_millis(100));
622            }
623        });
624
625        // zinit_logs() -> array
626        let h = handle.clone();
627        engine.register_fn(
628            "zinit_logs",
629            move || -> Result<Dynamic, Box<EvalAltResult>> {
630                match h.logs() {
631                    Ok(logs) => Ok(Dynamic::from_array(
632                        logs.into_iter().map(Dynamic::from).collect(),
633                    )),
634                    Err(e) => make_error(format!("zinit_logs failed: {}", e)),
635                }
636            },
637        );
638
639        // zinit_logs_filter(service) -> array
640        let h = handle.clone();
641        engine.register_fn(
642            "zinit_logs_filter",
643            move |service: &str| -> Result<Dynamic, Box<EvalAltResult>> {
644                let service = normalize_service_name(service);
645                match h.logs_filter(&service) {
646                    Ok(logs) => Ok(Dynamic::from_array(
647                        logs.into_iter().map(Dynamic::from).collect(),
648                    )),
649                    Err(e) => make_error(format!("zinit_logs_filter failed: {}", e)),
650                }
651            },
652        );
653
654        // zinit_logs_tail(n) -> array of last n log lines
655        let h = handle.clone();
656        engine.register_fn(
657            "zinit_logs_tail",
658            move |n: i64| -> Result<Dynamic, Box<EvalAltResult>> {
659                match h.logs() {
660                    Ok(logs) => {
661                        let n = n.max(0) as usize;
662                        let start = if logs.len() > n { logs.len() - n } else { 0 };
663                        Ok(Dynamic::from_array(
664                            logs[start..].iter().cloned().map(Dynamic::from).collect(),
665                        ))
666                    }
667                    Err(e) => make_error(format!("zinit_logs_tail failed: {}", e)),
668                }
669            },
670        );
671
672        // zinit_logs_print() - print all logs
673        let h = handle.clone();
674        engine.register_fn(
675            "zinit_logs_print",
676            move || -> Result<(), Box<EvalAltResult>> {
677                match h.logs() {
678                    Ok(logs) => {
679                        for line in logs {
680                            append_output(&line);
681                        }
682                        Ok(())
683                    }
684                    Err(e) => make_error(format!("zinit_logs_print failed: {}", e)),
685                }
686            },
687        );
688
689        // zinit_logs_print_filter(service) - print logs for a specific service
690        let h = handle.clone();
691        engine.register_fn(
692            "zinit_logs_print_filter",
693            move |service: &str| -> Result<(), Box<EvalAltResult>> {
694                let service = normalize_service_name(service);
695                match h.logs_filter(&service) {
696                    Ok(logs) => {
697                        for line in logs {
698                            append_output(&line);
699                        }
700                        Ok(())
701                    }
702                    Err(e) => make_error(format!("zinit_logs_print_filter failed: {}", e)),
703                }
704            },
705        );
706
707        // zinit_logs_follow(secs) - follow all logs for n seconds
708        let h = handle.clone();
709        engine.register_fn(
710            "zinit_logs_follow",
711            move |secs: i64| -> Result<(), Box<EvalAltResult>> {
712                let deadline = std::time::Instant::now() + Duration::from_secs(secs.max(1) as u64);
713                let mut last_count = 0usize;
714                while std::time::Instant::now() < deadline {
715                    if let Ok(logs) = h.logs() {
716                        if logs.len() > last_count {
717                            for line in &logs[last_count..] {
718                                append_output(line);
719                            }
720                            last_count = logs.len();
721                        }
722                    }
723                    std::thread::sleep(Duration::from_millis(100));
724                }
725                Ok(())
726            },
727        );
728
729        // zinit_logs_follow_filter(service, secs) - follow service logs for n seconds
730        let h = handle.clone();
731        engine.register_fn(
732            "zinit_logs_follow_filter",
733            move |service: &str, secs: i64| -> Result<(), Box<EvalAltResult>> {
734                let service = normalize_service_name(service);
735                let deadline = std::time::Instant::now() + Duration::from_secs(secs.max(1) as u64);
736                let mut last_count = 0usize;
737                while std::time::Instant::now() < deadline {
738                    if let Ok(logs) = h.logs_filter(&service) {
739                        if logs.len() > last_count {
740                            for line in &logs[last_count..] {
741                                append_output(line);
742                            }
743                            last_count = logs.len();
744                        }
745                    }
746                    std::thread::sleep(Duration::from_millis(100));
747                }
748                Ok(())
749            },
750        );
751
752        // zinit_start_all()
753        let h = handle.clone();
754        engine.register_fn(
755            "zinit_start_all",
756            move || -> Result<(), Box<EvalAltResult>> {
757                match h.start_all() {
758                    Ok(_) => {
759                        append_output("Started all services");
760                        Ok(())
761                    }
762                    Err(e) => make_error(format!("zinit_start_all failed: {}", e)),
763                }
764            },
765        );
766
767        // zinit_stop_all()
768        let h = handle.clone();
769        engine.register_fn(
770            "zinit_stop_all",
771            move || -> Result<(), Box<EvalAltResult>> {
772                match h.stop_all() {
773                    Ok(_) => {
774                        append_output("Stopped all services");
775                        Ok(())
776                    }
777                    Err(e) => make_error(format!("zinit_stop_all failed: {}", e)),
778                }
779            },
780        );
781
782        // zinit_service_delete_all() - stop and delete all services
783        let h = handle.clone();
784        engine.register_fn(
785            "zinit_service_delete_all",
786            move || -> Result<(), Box<EvalAltResult>> {
787                match h.delete_all() {
788                    Ok(_) => {
789                        append_output("Deleted all services");
790                        Ok(())
791                    }
792                    Err(e) => make_error(format!("zinit_service_delete_all failed: {}", e)),
793                }
794            },
795        );
796
797        // zinit_shutdown() - shutdown the server
798        let h = handle.clone();
799        engine.register_fn(
800            "zinit_shutdown",
801            move || -> Result<(), Box<EvalAltResult>> {
802                match h.shutdown() {
803                    Ok(_) => {
804                        append_output("Server shutting down");
805                        Ok(())
806                    }
807                    Err(e) => make_error(format!("zinit_shutdown failed: {}", e)),
808                }
809            },
810        );
811
812        // zinit_reboot() - reboot the system
813        let h = handle.clone();
814        engine.register_fn("zinit_reboot", move || -> Result<(), Box<EvalAltResult>> {
815            match h.reboot() {
816                Ok(_) => {
817                    append_output("System rebooting");
818                    Ok(())
819                }
820                Err(e) => make_error(format!("zinit_reboot failed: {}", e)),
821            }
822        });
823
824        // ============ Xinit Functions ============
825
826        // xinit_list() -> array
827        let h = handle.clone();
828        engine.register_fn(
829            "xinit_list",
830            move || -> Result<Dynamic, Box<EvalAltResult>> {
831                match h.xinit_list() {
832                    Ok(proxies) => Ok(Dynamic::from_array(
833                        proxies.into_iter().map(Dynamic::from).collect(),
834                    )),
835                    Err(e) => make_error(format!("xinit_list failed: {}", e)),
836                }
837            },
838        );
839
840        // xinit_list_md() -> prints markdown table of all proxies
841        let h = handle.clone();
842        engine.register_fn(
843            "xinit_list_md",
844            move || -> Result<(), Box<EvalAltResult>> {
845                match h.xinit_status_all() {
846                    Ok(statuses) => {
847                        if statuses.is_empty() {
848                            append_output("No xinit proxies registered.");
849                        } else {
850                            let mut output = String::new();
851                            output.push_str(
852                                "| Name | Listen | Backend | Service | Running | Connections |\n",
853                            );
854                            output.push_str(
855                                "|------|--------|---------|---------|---------|-------------|\n",
856                            );
857                            for status in statuses {
858                                output.push_str(&format!(
859                                    "| {} | {} | {} | {} | {} | {} |\n",
860                                    status.name,
861                                    status.listen,
862                                    status.backend,
863                                    status.service,
864                                    if status.running { "Yes" } else { "No" },
865                                    status.active_connections
866                                ));
867                            }
868                            append_output(&output);
869                        }
870                        Ok(())
871                    }
872                    Err(e) => make_error(format!("xinit_list_md failed: {}", e)),
873                }
874            },
875        );
876
877        // xinit_register(name, listen, backend, service)
878        // Legacy xinit_register function (deprecated - use xinit_proxy_new().register() instead)
879        let h = handle.clone();
880        engine.register_fn(
881            "xinit_register",
882            move |name: &str,
883                  listen: &str,
884                  backend: &str,
885                  service: &str|
886                  -> Result<(), Box<EvalAltResult>> {
887                let listen_addrs = vec![listen.to_string()];
888                match h.xinit_register(name, &listen_addrs, backend, service, 0, 30) {
889                    Ok(_) => {
890                        append_output(&format!(
891                            "Registered xinit proxy '{}': {} -> {}",
892                            name, listen, backend
893                        ));
894                        Ok(())
895                    }
896                    Err(e) => make_error(format!("xinit_register '{}' failed: {}", name, e)),
897                }
898            },
899        );
900
901        // xinit_unregister(name)
902        let h = handle.clone();
903        engine.register_fn(
904            "xinit_unregister",
905            move |name: &str| -> Result<(), Box<EvalAltResult>> {
906                match h.xinit_unregister(name) {
907                    Ok(_) => {
908                        append_output(&format!("Unregistered xinit proxy '{}'", name));
909                        Ok(())
910                    }
911                    Err(e) => make_error(format!("xinit_unregister '{}' failed: {}", name, e)),
912                }
913            },
914        );
915
916        // xinit_status(name) -> map
917        let h = handle.clone();
918        engine.register_fn(
919            "xinit_status",
920            move |name: &str| -> Result<Dynamic, Box<EvalAltResult>> {
921                match h.xinit_status(name) {
922                    Ok(status) => {
923                        let mut map = Map::new();
924                        map.insert("name".into(), status.name.into());
925                        map.insert("listen".into(), status.listen.into());
926                        map.insert("backend".into(), status.backend.into());
927                        map.insert("service".into(), status.service.into());
928                        map.insert("running".into(), status.running.into());
929                        map.insert(
930                            "total_connections".into(),
931                            (status.total_connections as i64).into(),
932                        );
933                        map.insert(
934                            "active_connections".into(),
935                            (status.active_connections as i64).into(),
936                        );
937                        map.insert(
938                            "bytes_to_backend".into(),
939                            (status.bytes_to_backend as i64).into(),
940                        );
941                        map.insert(
942                            "bytes_from_backend".into(),
943                            (status.bytes_from_backend as i64).into(),
944                        );
945                        Ok(Dynamic::from_map(map))
946                    }
947                    Err(e) => make_error(format!("xinit_status '{}' failed: {}", name, e)),
948                }
949            },
950        );
951
952        // xinit_status_all() -> array of maps
953        let h = handle.clone();
954        engine.register_fn(
955            "xinit_status_all",
956            move || -> Result<Dynamic, Box<EvalAltResult>> {
957                match h.xinit_status_all() {
958                    Ok(statuses) => {
959                        let arr: Vec<Dynamic> = statuses
960                            .into_iter()
961                            .map(|status| {
962                                let mut map = Map::new();
963                                map.insert("name".into(), status.name.into());
964                                map.insert("listen".into(), status.listen.into());
965                                map.insert("backend".into(), status.backend.into());
966                                map.insert("service".into(), status.service.into());
967                                map.insert("running".into(), status.running.into());
968                                map.insert(
969                                    "total_connections".into(),
970                                    (status.total_connections as i64).into(),
971                                );
972                                map.insert(
973                                    "active_connections".into(),
974                                    (status.active_connections as i64).into(),
975                                );
976                                Dynamic::from_map(map)
977                            })
978                            .collect();
979                        Ok(Dynamic::from_array(arr))
980                    }
981                    Err(e) => make_error(format!("xinit_status_all failed: {}", e)),
982                }
983            },
984        );
985    }
986
987    /// Compile a script
988    pub fn compile(&self, script: &str) -> Result<AST, Box<EvalAltResult>> {
989        self.engine.compile(script).map_err(|e| {
990            Box::new(EvalAltResult::ErrorRuntime(
991                Dynamic::from(e.to_string()),
992                Position::NONE,
993            ))
994        })
995    }
996
997    /// Run a compiled AST
998    pub fn run_ast(&self, ast: &AST) -> Result<Dynamic, Box<EvalAltResult>> {
999        let mut scope = Scope::new();
1000        self.engine.eval_ast_with_scope(&mut scope, ast)
1001    }
1002
1003    /// Run a script directly
1004    pub fn run(&self, script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
1005        let mut scope = Scope::new();
1006        self.engine.eval_with_scope(&mut scope, script)
1007    }
1008
1009    /// Run a script file (strips shebang line if present for shebang support)
1010    pub fn run_file(&self, path: &Path) -> Result<Dynamic, Box<EvalAltResult>> {
1011        let script = std::fs::read_to_string(path).map_err(|e| {
1012            Box::new(EvalAltResult::ErrorRuntime(
1013                Dynamic::from(format!("Failed to read file: {}", e)),
1014                Position::NONE,
1015            ))
1016        })?;
1017
1018        // Strip shebang line if present (for #!/usr/bin/env zinit support)
1019        let script = if script.starts_with("#!") {
1020            script.lines().skip(1).collect::<Vec<_>>().join("\n")
1021        } else {
1022            script
1023        };
1024
1025        self.run(&script)
1026    }
1027
1028    /// Get the underlying engine for advanced usage
1029    pub fn engine(&self) -> &Engine {
1030        &self.engine
1031    }
1032
1033    /// Get mutable engine for advanced usage
1034    pub fn engine_mut(&mut self) -> &mut Engine {
1035        &mut self.engine
1036    }
1037}
1038
1039impl Default for ZinitEngine {
1040    fn default() -> Self {
1041        Self::new()
1042    }
1043}
1044
1045/// Create a new engine with default handle
1046pub fn create_engine() -> Result<ZinitEngine, anyhow::Error> {
1047    let handle = ZinitHandle::new()?;
1048    Ok(ZinitEngine::with_handle(handle))
1049}
1050
1051/// Run a script with default handle
1052pub fn run_script(script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
1053    let engine = create_engine().map_err(|e| {
1054        Box::new(EvalAltResult::ErrorRuntime(
1055            Dynamic::from(e.to_string()),
1056            Position::NONE,
1057        ))
1058    })?;
1059    engine.run(script)
1060}
1061
1062/// Run a script file with default handle
1063pub fn run_script_file(path: &Path) -> Result<Dynamic, Box<EvalAltResult>> {
1064    let engine = create_engine().map_err(|e| {
1065        Box::new(EvalAltResult::ErrorRuntime(
1066            Dynamic::from(e.to_string()),
1067            Position::NONE,
1068        ))
1069    })?;
1070    engine.run_file(path)
1071}