1use crate::host::{with_host, JsObj};
10use fusevm::Value;
11use indexmap::IndexMap;
12
13pub const METHODS: &[&str] = &[
15 "cwd",
16 "chdir",
17 "exit",
18 "hrtime",
19 "uptime",
20 "memoryUsage",
21 "cpuUsage",
22 "umask",
23 "binding",
24 "emit",
25 "on",
26 "once",
27 "off",
28 "addListener",
29 "removeListener",
30 "removeAllListeners",
31 "listeners",
32 "emitWarning",
33 "kill",
34 "getuid",
35 "getgid",
36 "geteuid",
37 "getegid",
38 "getgroups",
39 "setuid",
40 "setgid",
41 "seteuid",
42 "setegid",
43 "setgroups",
44 "initgroups",
45 "ref",
46 "unref",
47 "abort",
48 "getActiveResourcesInfo",
49 "resourceUsage",
50 "threadCpuUsage",
51 "availableMemory",
52 "constrainedMemory",
53 "getBuiltinModule",
54 "openStdin",
55 "hasUncaughtExceptionCaptureCallback",
56 "setUncaughtExceptionCaptureCallback",
57 "addUncaughtExceptionCaptureCallback",
58 "execve",
59 "reallyExit",
60 "loadEnvFile",
61 "setSourceMapsEnabled",
62];
63
64thread_local! {
65 static UNCAUGHT_CAPTURE: std::cell::RefCell<Option<Value>> =
69 const { std::cell::RefCell::new(None) };
70}
71
72pub fn constant(name: &str) -> Option<Value> {
74 Some(match name {
75 "env" => env_object(),
76 "argv" => argv(),
77 "argv0" => with_host(|h| h.new_str(exec_path())),
78 "execPath" => with_host(|h| h.new_str(exec_path())),
79 "execArgv" => with_host(|h| h.new_array(Vec::new())),
80 "platform" => with_host(|h| h.new_str(super::os::platform())),
81 "arch" => with_host(|h| h.new_str(super::os::arch())),
82 "pid" => Value::Float(std::process::id() as f64),
83 "ppid" => Value::Float(0.0),
84 "title" => with_host(|h| h.new_str("node")),
85 "version" => with_host(|h| h.new_str("v26.5.0")),
88 "versions" => versions(),
89 "stdout" => std_stream(1),
90 "stderr" => std_stream(2),
91 "stdin" => std_stream(0),
92 _ => return None,
93 })
94}
95
96pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
97 Some(match method {
98 "cwd" => {
99 let d = std::env::current_dir()
100 .map(|p| p.to_string_lossy().into_owned())
101 .unwrap_or_default();
102 Ok(with_host(|h| h.new_str(d)))
103 }
104 "hrtime" => Ok(hrtime(args)),
108 "uptime" => Ok(Value::Float(0.0)),
109 "memoryUsage" => Ok(memory_usage()),
110 "cpuUsage" => Ok(with_host(|h| {
111 let mut m = IndexMap::new();
112 m.insert("user".into(), Value::Float(0.0));
113 m.insert("system".into(), Value::Float(0.0));
114 h.new_object(m)
115 })),
116 "umask" => Ok(Value::Float(0.0)),
117 "binding" => Err(crate::host::type_error("process.binding is not supported")),
118 "on" | "once" | "off" | "addListener" | "removeListener" | "removeAllListeners" => {
121 Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
122 }
123 "listeners" => Ok(with_host(|h| h.new_array(Vec::new()))),
124 "emit" => Ok(Value::Bool(false)),
125 "emitWarning" => Ok(Value::Undef),
126 "chdir" | "exit" | "kill" | "reallyExit" | "setSourceMapsEnabled" => Ok(Value::Undef),
127
128 "getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
130 "geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
131 "getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
132 "getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
133 "getgroups" => {
134 let groups = supplementary_groups();
135 Ok(with_host(|h| {
136 h.new_array(groups.into_iter().map(Value::Float).collect())
137 }))
138 }
139
140 "setuid" | "seteuid" | "setgid" | "setegid" => {
143 let id = super::arg_num(args, 0);
144 if id.is_finite() {
145 let id = id as u32;
146 unsafe {
148 match method {
149 "setuid" => libc::setuid(id),
150 "seteuid" => libc::seteuid(id),
151 "setgid" => libc::setgid(id),
152 _ => libc::setegid(id),
153 };
154 }
155 }
156 Ok(Value::Undef)
157 }
158 "setgroups" => {
159 let groups = gid_array(args.first());
160 unsafe {
162 libc::setgroups(groups.len() as _, groups.as_ptr());
163 }
164 Ok(Value::Undef)
165 }
166 "initgroups" => {
167 let user = super::arg_str(args, 0);
168 let extra = super::arg_num(args, 1);
169 if let Ok(c) = std::ffi::CString::new(user) {
170 let gid = if extra.is_finite() { extra as u32 } else { 0 };
171 unsafe {
173 libc::initgroups(c.as_ptr(), gid as _);
174 }
175 }
176 Ok(Value::Undef)
177 }
178
179 "ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
182 "abort" => std::process::abort(),
183 "getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
184 "resourceUsage" => Ok(resource_usage()),
185 "threadCpuUsage" => Ok(thread_cpu_usage()),
186 "availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
187 "getBuiltinModule" => {
188 let id = super::arg_str(args, 0);
189 let id = id.strip_prefix("node:").unwrap_or(&id);
190 match crate::stdlib::resolve(id) {
191 Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
192 None => Ok(Value::Undef),
193 }
194 }
195 "openStdin" => Ok(std_stream(0)),
196
197 "hasUncaughtExceptionCaptureCallback" => {
198 Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
199 }
200 "setUncaughtExceptionCaptureCallback" => {
201 let cb = args.first().cloned().unwrap_or(Value::Undef);
202 let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
203 if clear {
204 UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
205 } else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
206 return Some(Err(crate::host::type_error(
207 "`process.setUncaughtExceptionCaptureCallback()` was called \
208 while a capture callback was already active",
209 )));
210 } else {
211 UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
212 }
213 Ok(Value::Undef)
214 }
215 "addUncaughtExceptionCaptureCallback" => {
216 let cb = args.first().cloned().unwrap_or(Value::Undef);
217 if !matches!(cb, Value::Undef) {
218 UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
219 }
220 Ok(Value::Undef)
221 }
222 "execve" => exec_ve(args),
223 "loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
224 _ => return None,
225 })
226}
227
228fn env_object() -> Value {
230 with_host(|h| {
231 let mut m = IndexMap::new();
232 for (k, v) in std::env::vars() {
233 m.insert(k, h.new_str(v));
234 }
235 h.new_object(m)
236 })
237}
238
239fn argv() -> Value {
241 with_host(|h| {
242 let items: Vec<Value> = std::env::args().map(|a| h.new_str(a)).collect();
243 h.new_array(items)
244 })
245}
246
247fn exec_path() -> String {
248 std::env::current_exe()
249 .map(|p| p.to_string_lossy().into_owned())
250 .unwrap_or_else(|_| "node".into())
251}
252
253fn versions() -> Value {
255 with_host(|h| {
256 let mut m = IndexMap::new();
257 m.insert("node".into(), h.new_str("26.5.0"));
258 m.insert("v8".into(), h.new_str("0.0.0"));
259 h.new_object(m)
260 })
261}
262
263fn std_stream(fd: i32) -> Value {
267 with_host(|h| {
268 let mut m = IndexMap::new();
269 m.insert("@@native".into(), h.new_str("WriteStream"));
270 m.insert("fd".into(), Value::Float(fd as f64));
271 let is_tty = unsafe { libc::isatty(fd) == 1 };
273 m.insert("isTTY".into(), Value::Bool(is_tty));
274 m.insert("writable".into(), Value::Bool(fd != 0));
275 m.insert("readable".into(), Value::Bool(fd == 0));
276 if is_tty {
278 if let Some((cols, rows)) = super::tty::window_size(fd) {
279 m.insert("columns".into(), Value::Float(cols as f64));
280 m.insert("rows".into(), Value::Float(rows as f64));
281 }
282 }
283 h.new_object(m)
284 })
285}
286
287pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
291 match method {
292 "write" | "end" => {
293 let fd = with_host(|h| match h.get(recv) {
294 Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
295 _ => 1.0,
296 });
297 let chunk = super::arg_str(args, 0);
298 use std::io::Write as _;
299 if fd == 2.0 {
300 let _ = std::io::stderr().write_all(chunk.as_bytes());
301 let _ = std::io::stderr().flush();
302 } else {
303 let _ = std::io::stdout().write_all(chunk.as_bytes());
304 let _ = std::io::stdout().flush();
305 }
306 Ok(Value::Bool(true))
307 }
308 "on" | "once" | "removeListener" | "cork" | "uncork" | "setEncoding" => Ok(recv.clone()),
310 "cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
313 let seq = tty_control(method, args);
314 write_fd(stream_fd(recv), seq.as_bytes());
315 Ok(Value::Bool(true))
316 }
317 "getWindowSize" => {
318 let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
319 Ok(with_host(|h| {
320 h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
321 }))
322 }
323 "getColorDepth" => Ok(Value::Float(24.0)),
326 "hasColors" => Ok(Value::Bool(true)),
327 _ => Err(crate::host::type_error(&format!(
328 "{method} is not a function"
329 ))),
330 }
331}
332
333fn hrtime(args: &[Value]) -> Value {
334 let now = std::time::SystemTime::now()
335 .duration_since(std::time::UNIX_EPOCH)
336 .unwrap_or_default();
337 let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
338 if let Some(Value::Obj(_)) = args.first() {
340 if let Some(prev) = with_host(|h| match h.get(&args[0]) {
341 Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
342 _ => None,
343 }) {
344 secs -= prev.0;
345 nanos -= prev.1;
346 }
347 }
348 with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
349}
350
351fn memory_usage() -> Value {
352 with_host(|h| {
353 let mut m = IndexMap::new();
354 for k in ["rss", "heapTotal", "heapUsed", "external", "arrayBuffers"] {
355 m.insert(k.into(), Value::Float(0.0));
356 }
357 h.new_object(m)
358 })
359}
360
361fn stream_fd(recv: &Value) -> f64 {
363 with_host(|h| match h.get(recv) {
364 Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
365 _ => 1.0,
366 })
367}
368
369fn write_fd(fd: f64, bytes: &[u8]) {
371 use std::io::Write as _;
372 if fd == 2.0 {
373 let _ = std::io::stderr().write_all(bytes);
374 let _ = std::io::stderr().flush();
375 } else {
376 let _ = std::io::stdout().write_all(bytes);
377 let _ = std::io::stdout().flush();
378 }
379}
380
381fn tty_control(method: &str, args: &[Value]) -> String {
383 match method {
384 "cursorTo" => {
386 let x = super::arg_num(args, 0);
387 let y = super::arg_num(args, 1);
388 let x = if x.is_finite() { x as i64 } else { 0 };
389 if y.is_finite() {
390 format!("\x1b[{};{}H", y as i64 + 1, x + 1)
391 } else {
392 format!("\x1b[{}G", x + 1)
393 }
394 }
395 "moveCursor" => {
397 let dx = super::arg_num(args, 0);
398 let dy = super::arg_num(args, 1);
399 let mut s = String::new();
400 let dx = if dx.is_finite() { dx as i64 } else { 0 };
401 let dy = if dy.is_finite() { dy as i64 } else { 0 };
402 if dx > 0 {
403 s.push_str(&format!("\x1b[{dx}C"));
404 } else if dx < 0 {
405 s.push_str(&format!("\x1b[{}D", -dx));
406 }
407 if dy > 0 {
408 s.push_str(&format!("\x1b[{dy}B"));
409 } else if dy < 0 {
410 s.push_str(&format!("\x1b[{}A", -dy));
411 }
412 s
413 }
414 "clearLine" => match super::arg_num(args, 0) {
416 d if d < 0.0 => "\x1b[1K".into(),
417 d if d > 0.0 => "\x1b[0K".into(),
418 _ => "\x1b[2K".into(),
419 },
420 _ => "\x1b[0J".into(),
422 }
423}
424
425fn supplementary_groups() -> Vec<f64> {
427 unsafe {
429 let n = libc::getgroups(0, std::ptr::null_mut());
430 if n <= 0 {
431 return Vec::new();
432 }
433 let mut buf = vec![0 as libc::gid_t; n as usize];
434 let filled = libc::getgroups(n, buf.as_mut_ptr());
435 if filled < 0 {
436 return Vec::new();
437 }
438 buf.truncate(filled as usize);
439 buf.into_iter().map(|g| g as f64).collect()
440 }
441}
442
443fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
445 let Some(v) = v else { return Vec::new() };
446 with_host(|h| match h.get(v) {
447 Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
448 _ => Vec::new(),
449 })
450}
451
452fn get_rusage() -> Option<libc::rusage> {
454 unsafe {
456 let mut ru: libc::rusage = std::mem::zeroed();
457 (libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
458 }
459}
460
461fn tv_micros(t: &libc::timeval) -> f64 {
463 t.tv_sec as f64 * 1e6 + t.tv_usec as f64
464}
465
466fn resource_usage() -> Value {
468 let ru = get_rusage();
469 with_host(|h| {
470 let mut m = IndexMap::new();
471 let (utime, stime) = ru
472 .as_ref()
473 .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
474 .unwrap_or((0.0, 0.0));
475 m.insert("userCPUTime".into(), Value::Float(utime));
476 m.insert("systemCPUTime".into(), Value::Float(stime));
477 let fields = [
478 ("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
479 ("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
480 ("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
481 ("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
482 ("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
483 ("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
484 ("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
485 ("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
486 ("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
487 ("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
488 ("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
489 ("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
490 ("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
491 (
492 "involuntaryContextSwitches",
493 ru.as_ref().map(|r| r.ru_nivcsw),
494 ),
495 ];
496 for (k, v) in fields {
497 m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
498 }
499 h.new_object(m)
500 })
501}
502
503fn thread_cpu_usage() -> Value {
506 let (u, s) = get_rusage()
507 .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
508 .unwrap_or((0.0, 0.0));
509 with_host(|h| {
510 let mut m = IndexMap::new();
511 m.insert("user".into(), Value::Float(u));
512 m.insert("system".into(), Value::Float(s));
513 h.new_object(m)
514 })
515}
516
517fn exec_ve(args: &[Value]) -> Result<Value, String> {
520 use std::ffi::CString;
521 let prog = CString::new(super::arg_str(args, 0))
522 .map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
523
524 let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
525 Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
526 _ => Vec::new(),
527 });
528 let env_strs: Vec<String> = {
529 let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
530 Some(JsObj::Object(p)) => Some(
531 p.iter()
532 .map(|(k, v)| format!("{k}={}", h.str_of(v)))
533 .collect::<Vec<_>>(),
534 ),
535 _ => None,
536 });
537 from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
538 };
539
540 let to_c = |s: String| {
541 CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
542 };
543 let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
544 let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
545
546 let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
547 argv_p.push(std::ptr::null());
548 let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
549 envp_p.push(std::ptr::null());
550
551 unsafe {
554 libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
555 }
556 Err(crate::host::type_error(&format!(
557 "process.execve failed: {}",
558 std::io::Error::last_os_error()
559 )))
560}
561
562fn load_env_file(path: &str) -> Result<Value, String> {
565 let path = if path.is_empty() { ".env" } else { path };
566 let text =
567 std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
568 for line in text.lines() {
569 let line = line.trim();
570 if line.is_empty() || line.starts_with('#') {
571 continue;
572 }
573 let line = line.strip_prefix("export ").unwrap_or(line);
574 let Some((key, val)) = line.split_once('=') else {
575 continue;
576 };
577 let key = key.trim();
578 if key.is_empty() {
579 continue;
580 }
581 let mut val = val.trim();
582 if val.len() >= 2
583 && ((val.starts_with('"') && val.ends_with('"'))
584 || (val.starts_with('\'') && val.ends_with('\'')))
585 {
586 val = &val[1..val.len() - 1];
587 }
588 std::env::set_var(key, val);
589 }
590 Ok(Value::Undef)
591}