use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
pub const METHODS: &[&str] = &[
"cwd",
"chdir",
"exit",
"hrtime",
"uptime",
"memoryUsage",
"cpuUsage",
"umask",
"binding",
"emit",
"on",
"once",
"off",
"addListener",
"removeListener",
"removeAllListeners",
"listeners",
"emitWarning",
"kill",
"getuid",
"getgid",
"geteuid",
"getegid",
"getgroups",
"setuid",
"setgid",
"seteuid",
"setegid",
"setgroups",
"initgroups",
"ref",
"unref",
"abort",
"getActiveResourcesInfo",
"resourceUsage",
"threadCpuUsage",
"availableMemory",
"constrainedMemory",
"getBuiltinModule",
"openStdin",
"hasUncaughtExceptionCaptureCallback",
"setUncaughtExceptionCaptureCallback",
"addUncaughtExceptionCaptureCallback",
"execve",
"reallyExit",
"loadEnvFile",
"setSourceMapsEnabled",
];
thread_local! {
static UNCAUGHT_CAPTURE: std::cell::RefCell<Option<Value>> =
const { std::cell::RefCell::new(None) };
}
pub fn constant(name: &str) -> Option<Value> {
Some(match name {
"env" => env_object(),
"argv" => argv(),
"argv0" => with_host(|h| h.new_str(exec_path())),
"execPath" => with_host(|h| h.new_str(exec_path())),
"execArgv" => with_host(|h| h.new_array(Vec::new())),
"platform" => with_host(|h| h.new_str(super::os::platform())),
"arch" => with_host(|h| h.new_str(super::os::arch())),
"pid" => Value::Float(std::process::id() as f64),
"ppid" => Value::Float(0.0),
"title" => with_host(|h| h.new_str("node")),
"version" => with_host(|h| h.new_str("v26.5.0")),
"versions" => versions(),
"stdout" => std_stream(1),
"stderr" => std_stream(2),
"stdin" => std_stream(0),
_ => return None,
})
}
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"cwd" => {
let d = std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
Ok(with_host(|h| h.new_str(d)))
}
"hrtime" => Ok(hrtime(args)),
"uptime" => Ok(Value::Float(0.0)),
"memoryUsage" => Ok(memory_usage()),
"cpuUsage" => Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("user".into(), Value::Float(0.0));
m.insert("system".into(), Value::Float(0.0));
h.new_object(m)
})),
"umask" => Ok(Value::Float(0.0)),
"binding" => Err(crate::host::type_error("process.binding is not supported")),
"on" | "once" | "off" | "addListener" | "removeListener" | "removeAllListeners" => {
Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
}
"listeners" => Ok(with_host(|h| h.new_array(Vec::new()))),
"emit" => Ok(Value::Bool(false)),
"emitWarning" => Ok(Value::Undef),
"chdir" | "exit" | "kill" | "reallyExit" | "setSourceMapsEnabled" => Ok(Value::Undef),
"getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
"geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
"getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
"getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
"getgroups" => {
let groups = supplementary_groups();
Ok(with_host(|h| {
h.new_array(groups.into_iter().map(Value::Float).collect())
}))
}
"setuid" | "seteuid" | "setgid" | "setegid" => {
let id = super::arg_num(args, 0);
if id.is_finite() {
let id = id as u32;
unsafe {
match method {
"setuid" => libc::setuid(id),
"seteuid" => libc::seteuid(id),
"setgid" => libc::setgid(id),
_ => libc::setegid(id),
};
}
}
Ok(Value::Undef)
}
"setgroups" => {
let groups = gid_array(args.first());
unsafe {
libc::setgroups(groups.len() as _, groups.as_ptr());
}
Ok(Value::Undef)
}
"initgroups" => {
let user = super::arg_str(args, 0);
let extra = super::arg_num(args, 1);
if let Ok(c) = std::ffi::CString::new(user) {
let gid = if extra.is_finite() { extra as u32 } else { 0 };
unsafe {
libc::initgroups(c.as_ptr(), gid as _);
}
}
Ok(Value::Undef)
}
"ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
"abort" => std::process::abort(),
"getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
"resourceUsage" => Ok(resource_usage()),
"threadCpuUsage" => Ok(thread_cpu_usage()),
"availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
"getBuiltinModule" => {
let id = super::arg_str(args, 0);
let id = id.strip_prefix("node:").unwrap_or(&id);
match crate::stdlib::resolve(id) {
Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
None => Ok(Value::Undef),
}
}
"openStdin" => Ok(std_stream(0)),
"hasUncaughtExceptionCaptureCallback" => {
Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
}
"setUncaughtExceptionCaptureCallback" => {
let cb = args.first().cloned().unwrap_or(Value::Undef);
let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
if clear {
UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
} else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
return Some(Err(crate::host::type_error(
"`process.setUncaughtExceptionCaptureCallback()` was called \
while a capture callback was already active",
)));
} else {
UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
}
Ok(Value::Undef)
}
"addUncaughtExceptionCaptureCallback" => {
let cb = args.first().cloned().unwrap_or(Value::Undef);
if !matches!(cb, Value::Undef) {
UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
}
Ok(Value::Undef)
}
"execve" => exec_ve(args),
"loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
_ => return None,
})
}
fn env_object() -> Value {
with_host(|h| {
let mut m = IndexMap::new();
for (k, v) in std::env::vars() {
m.insert(k, h.new_str(v));
}
h.new_object(m)
})
}
fn argv() -> Value {
with_host(|h| {
let items: Vec<Value> = std::env::args().map(|a| h.new_str(a)).collect();
h.new_array(items)
})
}
fn exec_path() -> String {
std::env::current_exe()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "node".into())
}
fn versions() -> Value {
with_host(|h| {
let mut m = IndexMap::new();
m.insert("node".into(), h.new_str("26.5.0"));
m.insert("v8".into(), h.new_str("0.0.0"));
h.new_object(m)
})
}
fn std_stream(fd: i32) -> Value {
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("WriteStream"));
m.insert("fd".into(), Value::Float(fd as f64));
let is_tty = unsafe { libc::isatty(fd) == 1 };
m.insert("isTTY".into(), Value::Bool(is_tty));
m.insert("writable".into(), Value::Bool(fd != 0));
m.insert("readable".into(), Value::Bool(fd == 0));
if is_tty {
if let Some((cols, rows)) = super::tty::window_size(fd) {
m.insert("columns".into(), Value::Float(cols as f64));
m.insert("rows".into(), Value::Float(rows as f64));
}
}
h.new_object(m)
})
}
pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"write" | "end" => {
let fd = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
_ => 1.0,
});
let chunk = super::arg_str(args, 0);
use std::io::Write as _;
if fd == 2.0 {
let _ = std::io::stderr().write_all(chunk.as_bytes());
let _ = std::io::stderr().flush();
} else {
let _ = std::io::stdout().write_all(chunk.as_bytes());
let _ = std::io::stdout().flush();
}
Ok(Value::Bool(true))
}
"on" | "once" | "removeListener" | "cork" | "uncork" | "setEncoding" => Ok(recv.clone()),
"cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
let seq = tty_control(method, args);
write_fd(stream_fd(recv), seq.as_bytes());
Ok(Value::Bool(true))
}
"getWindowSize" => {
let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
Ok(with_host(|h| {
h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
}))
}
"getColorDepth" => Ok(Value::Float(24.0)),
"hasColors" => Ok(Value::Bool(true)),
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn hrtime(args: &[Value]) -> Value {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
if let Some(Value::Obj(_)) = args.first() {
if let Some(prev) = with_host(|h| match h.get(&args[0]) {
Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
_ => None,
}) {
secs -= prev.0;
nanos -= prev.1;
}
}
with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
}
fn memory_usage() -> Value {
with_host(|h| {
let mut m = IndexMap::new();
for k in ["rss", "heapTotal", "heapUsed", "external", "arrayBuffers"] {
m.insert(k.into(), Value::Float(0.0));
}
h.new_object(m)
})
}
fn stream_fd(recv: &Value) -> f64 {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
_ => 1.0,
})
}
fn write_fd(fd: f64, bytes: &[u8]) {
use std::io::Write as _;
if fd == 2.0 {
let _ = std::io::stderr().write_all(bytes);
let _ = std::io::stderr().flush();
} else {
let _ = std::io::stdout().write_all(bytes);
let _ = std::io::stdout().flush();
}
}
fn tty_control(method: &str, args: &[Value]) -> String {
match method {
"cursorTo" => {
let x = super::arg_num(args, 0);
let y = super::arg_num(args, 1);
let x = if x.is_finite() { x as i64 } else { 0 };
if y.is_finite() {
format!("\x1b[{};{}H", y as i64 + 1, x + 1)
} else {
format!("\x1b[{}G", x + 1)
}
}
"moveCursor" => {
let dx = super::arg_num(args, 0);
let dy = super::arg_num(args, 1);
let mut s = String::new();
let dx = if dx.is_finite() { dx as i64 } else { 0 };
let dy = if dy.is_finite() { dy as i64 } else { 0 };
if dx > 0 {
s.push_str(&format!("\x1b[{dx}C"));
} else if dx < 0 {
s.push_str(&format!("\x1b[{}D", -dx));
}
if dy > 0 {
s.push_str(&format!("\x1b[{dy}B"));
} else if dy < 0 {
s.push_str(&format!("\x1b[{}A", -dy));
}
s
}
"clearLine" => match super::arg_num(args, 0) {
d if d < 0.0 => "\x1b[1K".into(),
d if d > 0.0 => "\x1b[0K".into(),
_ => "\x1b[2K".into(),
},
_ => "\x1b[0J".into(),
}
}
fn supplementary_groups() -> Vec<f64> {
unsafe {
let n = libc::getgroups(0, std::ptr::null_mut());
if n <= 0 {
return Vec::new();
}
let mut buf = vec![0 as libc::gid_t; n as usize];
let filled = libc::getgroups(n, buf.as_mut_ptr());
if filled < 0 {
return Vec::new();
}
buf.truncate(filled as usize);
buf.into_iter().map(|g| g as f64).collect()
}
}
fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
let Some(v) = v else { return Vec::new() };
with_host(|h| match h.get(v) {
Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
_ => Vec::new(),
})
}
fn get_rusage() -> Option<libc::rusage> {
unsafe {
let mut ru: libc::rusage = std::mem::zeroed();
(libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
}
}
fn tv_micros(t: &libc::timeval) -> f64 {
t.tv_sec as f64 * 1e6 + t.tv_usec as f64
}
fn resource_usage() -> Value {
let ru = get_rusage();
with_host(|h| {
let mut m = IndexMap::new();
let (utime, stime) = ru
.as_ref()
.map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
.unwrap_or((0.0, 0.0));
m.insert("userCPUTime".into(), Value::Float(utime));
m.insert("systemCPUTime".into(), Value::Float(stime));
let fields = [
("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
(
"involuntaryContextSwitches",
ru.as_ref().map(|r| r.ru_nivcsw),
),
];
for (k, v) in fields {
m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
}
h.new_object(m)
})
}
fn thread_cpu_usage() -> Value {
let (u, s) = get_rusage()
.map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
.unwrap_or((0.0, 0.0));
with_host(|h| {
let mut m = IndexMap::new();
m.insert("user".into(), Value::Float(u));
m.insert("system".into(), Value::Float(s));
h.new_object(m)
})
}
fn exec_ve(args: &[Value]) -> Result<Value, String> {
use std::ffi::CString;
let prog = CString::new(super::arg_str(args, 0))
.map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
_ => Vec::new(),
});
let env_strs: Vec<String> = {
let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
Some(JsObj::Object(p)) => Some(
p.iter()
.map(|(k, v)| format!("{k}={}", h.str_of(v)))
.collect::<Vec<_>>(),
),
_ => None,
});
from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
};
let to_c = |s: String| {
CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
};
let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
argv_p.push(std::ptr::null());
let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
envp_p.push(std::ptr::null());
unsafe {
libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
}
Err(crate::host::type_error(&format!(
"process.execve failed: {}",
std::io::Error::last_os_error()
)))
}
fn load_env_file(path: &str) -> Result<Value, String> {
let path = if path.is_empty() { ".env" } else { path };
let text =
std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let line = line.strip_prefix("export ").unwrap_or(line);
let Some((key, val)) = line.split_once('=') else {
continue;
};
let key = key.trim();
if key.is_empty() {
continue;
}
let mut val = val.trim();
if val.len() >= 2
&& ((val.starts_with('"') && val.ends_with('"'))
|| (val.starts_with('\'') && val.ends_with('\'')))
{
val = &val[1..val.len() - 1];
}
std::env::set_var(key, val);
}
Ok(Value::Undef)
}