harn_vm/stdlib/
process.rs1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4use std::rc::Rc;
5
6use crate::value::{VmError, VmValue};
7use crate::vm::Vm;
8
9thread_local! {
10 static VM_SOURCE_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
11}
12
13pub(crate) fn set_thread_source_dir(dir: &std::path::Path) {
15 VM_SOURCE_DIR.with(|sd| *sd.borrow_mut() = Some(dir.to_path_buf()));
16}
17
18pub(crate) fn reset_process_state() {
20 VM_SOURCE_DIR.with(|sd| *sd.borrow_mut() = None);
21}
22
23pub(crate) fn register_process_builtins(vm: &mut Vm) {
24 vm.register_builtin("env", |args, _out| {
25 let name = args.first().map(|a| a.display()).unwrap_or_default();
26 match std::env::var(&name) {
27 Ok(val) => Ok(VmValue::String(Rc::from(val))),
28 Err(_) => Ok(VmValue::Nil),
29 }
30 });
31
32 vm.register_builtin("timestamp", |_args, _out| {
33 use std::time::{SystemTime, UNIX_EPOCH};
34 let secs = SystemTime::now()
35 .duration_since(UNIX_EPOCH)
36 .map(|d| d.as_secs_f64())
37 .unwrap_or(0.0);
38 Ok(VmValue::Float(secs))
39 });
40
41 vm.register_builtin("exit", |args, _out| {
42 let code = args.first().and_then(|a| a.as_int()).unwrap_or(0);
43 std::process::exit(code as i32);
44 });
45
46 vm.register_builtin("exec", |args, _out| {
47 if args.is_empty() {
48 return Err(VmError::Thrown(VmValue::String(Rc::from(
49 "exec: command is required",
50 ))));
51 }
52 let cmd = args[0].display();
53 let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
54 let output = std::process::Command::new(&cmd)
55 .args(&cmd_args)
56 .output()
57 .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
58 Ok(vm_output_to_value(output))
59 });
60
61 vm.register_builtin("shell", |args, _out| {
62 let cmd = args.first().map(|a| a.display()).unwrap_or_default();
63 if cmd.is_empty() {
64 return Err(VmError::Thrown(VmValue::String(Rc::from(
65 "shell: command string is required",
66 ))));
67 }
68 let shell = if cfg!(target_os = "windows") {
69 "cmd"
70 } else {
71 "sh"
72 };
73 let flag = if cfg!(target_os = "windows") {
74 "/C"
75 } else {
76 "-c"
77 };
78 let output = std::process::Command::new(shell)
79 .arg(flag)
80 .arg(&cmd)
81 .output()
82 .map_err(|e| {
83 VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
84 })?;
85 Ok(vm_output_to_value(output))
86 });
87
88 vm.register_builtin("elapsed", |_args, _out| {
89 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
90 let start = START.get_or_init(std::time::Instant::now);
91 Ok(VmValue::Int(start.elapsed().as_millis() as i64))
92 });
93
94 vm.register_builtin("username", |_args, _out| {
97 let user = std::env::var("USER")
98 .or_else(|_| std::env::var("USERNAME"))
99 .unwrap_or_default();
100 Ok(VmValue::String(Rc::from(user)))
101 });
102
103 vm.register_builtin("hostname", |_args, _out| {
104 let name = std::env::var("HOSTNAME")
105 .or_else(|_| std::env::var("COMPUTERNAME"))
106 .or_else(|_| {
107 std::process::Command::new("hostname")
108 .output()
109 .ok()
110 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
111 .ok_or(std::env::VarError::NotPresent)
112 })
113 .unwrap_or_default();
114 Ok(VmValue::String(Rc::from(name)))
115 });
116
117 vm.register_builtin("platform", |_args, _out| {
118 let os = if cfg!(target_os = "macos") {
119 "darwin"
120 } else if cfg!(target_os = "linux") {
121 "linux"
122 } else if cfg!(target_os = "windows") {
123 "windows"
124 } else {
125 std::env::consts::OS
126 };
127 Ok(VmValue::String(Rc::from(os)))
128 });
129
130 vm.register_builtin("arch", |_args, _out| {
131 Ok(VmValue::String(Rc::from(std::env::consts::ARCH)))
132 });
133
134 vm.register_builtin("home_dir", |_args, _out| {
135 let home = std::env::var("HOME")
136 .or_else(|_| std::env::var("USERPROFILE"))
137 .unwrap_or_default();
138 Ok(VmValue::String(Rc::from(home)))
139 });
140
141 vm.register_builtin("pid", |_args, _out| {
142 Ok(VmValue::Int(std::process::id() as i64))
143 });
144
145 vm.register_builtin("date_iso", |_args, _out| {
148 use crate::stdlib::datetime::vm_civil_from_timestamp;
149 let now = std::time::SystemTime::now()
150 .duration_since(std::time::UNIX_EPOCH)
151 .unwrap_or_default();
152 let total_secs = now.as_secs();
153 let millis = now.subsec_millis();
154 let (y, m, d, hour, minute, second, _) = vm_civil_from_timestamp(total_secs);
155 Ok(VmValue::String(Rc::from(format!(
156 "{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z"
157 ))))
158 });
159
160 vm.register_builtin("cwd", |_args, _out| {
161 let dir = std::env::current_dir()
162 .map(|p| p.to_string_lossy().to_string())
163 .unwrap_or_default();
164 Ok(VmValue::String(Rc::from(dir)))
165 });
166}
167
168pub fn find_project_root(base: &std::path::Path) -> Option<std::path::PathBuf> {
170 let mut dir = base.to_path_buf();
171 loop {
172 if dir.join("harn.toml").exists() {
173 return Some(dir);
174 }
175 if !dir.pop() {
176 return None;
177 }
178 }
179}
180
181pub(crate) fn register_path_builtins(vm: &mut Vm) {
183 vm.register_builtin("source_dir", |_args, _out| {
184 let dir = VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
185 match dir {
186 Some(d) => Ok(VmValue::String(Rc::from(d.to_string_lossy().to_string()))),
187 None => {
188 let cwd = std::env::current_dir()
189 .map(|p| p.to_string_lossy().to_string())
190 .unwrap_or_default();
191 Ok(VmValue::String(Rc::from(cwd)))
192 }
193 }
194 });
195
196 vm.register_builtin("project_root", |_args, _out| {
197 let base = VM_SOURCE_DIR
198 .with(|sd| sd.borrow().clone())
199 .or_else(|| std::env::current_dir().ok())
200 .unwrap_or_else(|| PathBuf::from("."));
201 match find_project_root(&base) {
202 Some(root) => Ok(VmValue::String(Rc::from(
203 root.to_string_lossy().to_string(),
204 ))),
205 None => Ok(VmValue::Nil),
206 }
207 });
208}
209
210fn vm_output_to_value(output: std::process::Output) -> VmValue {
211 let mut result = BTreeMap::new();
212 result.insert(
213 "stdout".to_string(),
214 VmValue::String(Rc::from(
215 String::from_utf8_lossy(&output.stdout).to_string().as_str(),
216 )),
217 );
218 result.insert(
219 "stderr".to_string(),
220 VmValue::String(Rc::from(
221 String::from_utf8_lossy(&output.stderr).to_string().as_str(),
222 )),
223 );
224 result.insert(
225 "status".to_string(),
226 VmValue::Int(output.status.code().unwrap_or(-1) as i64),
227 );
228 result.insert(
229 "success".to_string(),
230 VmValue::Bool(output.status.success()),
231 );
232 VmValue::Dict(Rc::new(result))
233}