deno_os/
lib.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::env;
6use std::sync::atomic::AtomicI32;
7use std::sync::atomic::Ordering;
8use std::sync::Arc;
9
10use deno_core::op2;
11use deno_core::v8;
12use deno_core::OpState;
13use deno_path_util::normalize_path;
14use deno_permissions::PermissionCheckError;
15use deno_permissions::PermissionsContainer;
16use once_cell::sync::Lazy;
17use serde::Serialize;
18
19mod ops;
20pub mod signal;
21pub mod sys_info;
22
23pub use ops::signal::SignalError;
24
25pub static NODE_ENV_VAR_ALLOWLIST: Lazy<HashSet<String>> = Lazy::new(|| {
26  // The full list of environment variables supported by Node.js is available
27  // at https://nodejs.org/api/cli.html#environment-variables
28  let mut set = HashSet::new();
29  set.insert("NODE_DEBUG".to_string());
30  set.insert("NODE_OPTIONS".to_string());
31  set
32});
33
34#[derive(Clone, Default)]
35pub struct ExitCode(Arc<AtomicI32>);
36
37impl ExitCode {
38  pub fn get(&self) -> i32 {
39    self.0.load(Ordering::Relaxed)
40  }
41
42  pub fn set(&mut self, code: i32) {
43    self.0.store(code, Ordering::Relaxed);
44  }
45}
46
47pub fn exit(code: i32) -> ! {
48  deno_telemetry::flush();
49  #[allow(clippy::disallowed_methods)]
50  std::process::exit(code);
51}
52
53deno_core::extension!(
54  deno_os,
55  ops = [
56    op_env,
57    op_exec_path,
58    op_exit,
59    op_delete_env,
60    op_get_env,
61    op_gid,
62    op_hostname,
63    op_loadavg,
64    op_network_interfaces,
65    op_os_release,
66    op_os_uptime,
67    op_set_env,
68    op_set_exit_code,
69    op_get_exit_code,
70    op_system_memory_info,
71    op_uid,
72    op_runtime_cpu_usage,
73    op_runtime_memory_usage,
74    ops::signal::op_signal_bind,
75    ops::signal::op_signal_unbind,
76    ops::signal::op_signal_poll,
77  ],
78  esm = ["30_os.js", "40_signals.js"],
79  options = {
80    exit_code: ExitCode,
81  },
82  state = |state, options| {
83    state.put::<ExitCode>(options.exit_code);
84    #[cfg(unix)]
85    {
86      state.put(ops::signal::SignalState::default());
87    }
88  }
89);
90
91deno_core::extension!(
92  deno_os_worker,
93  ops = [
94    op_env,
95    op_exec_path,
96    op_exit,
97    op_delete_env,
98    op_get_env,
99    op_gid,
100    op_hostname,
101    op_loadavg,
102    op_network_interfaces,
103    op_os_release,
104    op_os_uptime,
105    op_set_env,
106    op_set_exit_code,
107    op_get_exit_code,
108    op_system_memory_info,
109    op_uid,
110    op_runtime_cpu_usage,
111    op_runtime_memory_usage,
112    ops::signal::op_signal_bind,
113    ops::signal::op_signal_unbind,
114    ops::signal::op_signal_poll,
115  ],
116  esm = ["30_os.js", "40_signals.js"],
117  middleware = |op| match op.name {
118    "op_exit" | "op_set_exit_code" | "op_get_exit_code" =>
119      op.with_implementation_from(&deno_core::op_void_sync()),
120    _ => op,
121  },
122  state = |state| {
123    #[cfg(unix)]
124    {
125      state.put(ops::signal::SignalState::default());
126    }
127  }
128);
129
130#[derive(Debug, thiserror::Error, deno_error::JsError)]
131pub enum OsError {
132  #[class(inherit)]
133  #[error(transparent)]
134  Permission(#[from] PermissionCheckError),
135  #[class("InvalidData")]
136  #[error("File name or path {0:?} is not valid UTF-8")]
137  InvalidUtf8(std::ffi::OsString),
138  #[class(type)]
139  #[error("Key is an empty string.")]
140  EnvEmptyKey,
141  #[class(type)]
142  #[error("Key contains invalid characters: {0:?}")]
143  EnvInvalidKey(String),
144  #[class(type)]
145  #[error("Value contains invalid characters: {0:?}")]
146  EnvInvalidValue(String),
147  #[class(inherit)]
148  #[error(transparent)]
149  Var(#[from] env::VarError),
150  #[class(inherit)]
151  #[error("{0}")]
152  Io(#[from] std::io::Error),
153}
154
155#[op2(stack_trace)]
156#[string]
157fn op_exec_path(state: &mut OpState) -> Result<String, OsError> {
158  let current_exe = env::current_exe().unwrap();
159  state
160    .borrow_mut::<PermissionsContainer>()
161    .check_read_blind(&current_exe, "exec_path", "Deno.execPath()")?;
162  // normalize path so it doesn't include '.' or '..' components
163  let path = normalize_path(current_exe);
164
165  path
166    .into_os_string()
167    .into_string()
168    .map_err(OsError::InvalidUtf8)
169}
170
171fn dt_change_notif(isolate: &mut v8::Isolate, key: &str) {
172  extern "C" {
173    #[cfg(unix)]
174    fn tzset();
175
176    #[cfg(windows)]
177    fn _tzset();
178  }
179
180  if key == "TZ" {
181    // SAFETY: tzset/_tzset (libc) is called to update the timezone information
182    unsafe {
183      #[cfg(unix)]
184      tzset();
185
186      #[cfg(windows)]
187      _tzset();
188    }
189
190    isolate.date_time_configuration_change_notification(
191      v8::TimeZoneDetection::Redetect,
192    );
193  }
194}
195
196#[op2(fast, stack_trace)]
197fn op_set_env(
198  state: &mut OpState,
199  scope: &mut v8::HandleScope,
200  #[string] key: &str,
201  #[string] value: &str,
202) -> Result<(), OsError> {
203  state.borrow_mut::<PermissionsContainer>().check_env(key)?;
204  if key.is_empty() {
205    return Err(OsError::EnvEmptyKey);
206  }
207  if key.contains(&['=', '\0'] as &[char]) {
208    return Err(OsError::EnvInvalidKey(key.to_string()));
209  }
210  if value.contains('\0') {
211    return Err(OsError::EnvInvalidValue(value.to_string()));
212  }
213
214  env::set_var(key, value);
215  dt_change_notif(scope, key);
216  Ok(())
217}
218
219#[op2(stack_trace)]
220#[serde]
221fn op_env(
222  state: &mut OpState,
223) -> Result<HashMap<String, String>, PermissionCheckError> {
224  state.borrow_mut::<PermissionsContainer>().check_env_all()?;
225  Ok(env::vars().collect())
226}
227
228#[op2(stack_trace)]
229#[string]
230fn op_get_env(
231  state: &mut OpState,
232  #[string] key: String,
233) -> Result<Option<String>, OsError> {
234  let skip_permission_check = NODE_ENV_VAR_ALLOWLIST.contains(&key);
235
236  if !skip_permission_check {
237    state.borrow_mut::<PermissionsContainer>().check_env(&key)?;
238  }
239
240  if key.is_empty() {
241    return Err(OsError::EnvEmptyKey);
242  }
243
244  if key.contains(&['=', '\0'] as &[char]) {
245    return Err(OsError::EnvInvalidKey(key.to_string()));
246  }
247
248  let r = match env::var(key) {
249    Err(env::VarError::NotPresent) => None,
250    v => Some(v?),
251  };
252  Ok(r)
253}
254
255#[op2(fast, stack_trace)]
256fn op_delete_env(
257  state: &mut OpState,
258  #[string] key: String,
259) -> Result<(), OsError> {
260  state.borrow_mut::<PermissionsContainer>().check_env(&key)?;
261  if key.is_empty() || key.contains(&['=', '\0'] as &[char]) {
262    return Err(OsError::EnvInvalidKey(key.to_string()));
263  }
264  env::remove_var(key);
265  Ok(())
266}
267
268#[op2(fast)]
269fn op_set_exit_code(state: &mut OpState, #[smi] code: i32) {
270  state.borrow_mut::<ExitCode>().set(code);
271}
272
273#[op2(fast)]
274#[smi]
275fn op_get_exit_code(state: &mut OpState) -> i32 {
276  state.borrow_mut::<ExitCode>().get()
277}
278
279#[op2(fast)]
280fn op_exit(state: &mut OpState) {
281  let code = state.borrow::<ExitCode>().get();
282  exit(code)
283}
284
285#[op2(stack_trace)]
286#[serde]
287fn op_loadavg(
288  state: &mut OpState,
289) -> Result<(f64, f64, f64), PermissionCheckError> {
290  state
291    .borrow_mut::<PermissionsContainer>()
292    .check_sys("loadavg", "Deno.loadavg()")?;
293  Ok(sys_info::loadavg())
294}
295
296#[op2(stack_trace, stack_trace)]
297#[string]
298fn op_hostname(state: &mut OpState) -> Result<String, PermissionCheckError> {
299  state
300    .borrow_mut::<PermissionsContainer>()
301    .check_sys("hostname", "Deno.hostname()")?;
302  Ok(sys_info::hostname())
303}
304
305#[op2(stack_trace)]
306#[string]
307fn op_os_release(state: &mut OpState) -> Result<String, PermissionCheckError> {
308  state
309    .borrow_mut::<PermissionsContainer>()
310    .check_sys("osRelease", "Deno.osRelease()")?;
311  Ok(sys_info::os_release())
312}
313
314#[op2(stack_trace)]
315#[serde]
316fn op_network_interfaces(
317  state: &mut OpState,
318) -> Result<Vec<NetworkInterface>, OsError> {
319  state
320    .borrow_mut::<PermissionsContainer>()
321    .check_sys("networkInterfaces", "Deno.networkInterfaces()")?;
322  Ok(netif::up()?.map(NetworkInterface::from).collect())
323}
324
325#[derive(Serialize)]
326struct NetworkInterface {
327  family: &'static str,
328  name: String,
329  address: String,
330  netmask: String,
331  scopeid: Option<u32>,
332  cidr: String,
333  mac: String,
334}
335
336impl From<netif::Interface> for NetworkInterface {
337  fn from(ifa: netif::Interface) -> Self {
338    let family = match ifa.address() {
339      std::net::IpAddr::V4(_) => "IPv4",
340      std::net::IpAddr::V6(_) => "IPv6",
341    };
342
343    let (address, range) = ifa.cidr();
344    let cidr = format!("{address:?}/{range}");
345
346    let name = ifa.name().to_owned();
347    let address = format!("{:?}", ifa.address());
348    let netmask = format!("{:?}", ifa.netmask());
349    let scopeid = ifa.scope_id();
350
351    let [b0, b1, b2, b3, b4, b5] = ifa.mac();
352    let mac = format!("{b0:02x}:{b1:02x}:{b2:02x}:{b3:02x}:{b4:02x}:{b5:02x}");
353
354    Self {
355      family,
356      name,
357      address,
358      netmask,
359      scopeid,
360      cidr,
361      mac,
362    }
363  }
364}
365
366#[op2(stack_trace)]
367#[serde]
368fn op_system_memory_info(
369  state: &mut OpState,
370) -> Result<Option<sys_info::MemInfo>, PermissionCheckError> {
371  state
372    .borrow_mut::<PermissionsContainer>()
373    .check_sys("systemMemoryInfo", "Deno.systemMemoryInfo()")?;
374  Ok(sys_info::mem_info())
375}
376
377#[cfg(not(windows))]
378#[op2(stack_trace)]
379#[smi]
380fn op_gid(state: &mut OpState) -> Result<Option<u32>, PermissionCheckError> {
381  state
382    .borrow_mut::<PermissionsContainer>()
383    .check_sys("gid", "Deno.gid()")?;
384  // TODO(bartlomieju):
385  #[allow(clippy::undocumented_unsafe_blocks)]
386  unsafe {
387    Ok(Some(libc::getgid()))
388  }
389}
390
391#[cfg(windows)]
392#[op2(stack_trace)]
393#[smi]
394fn op_gid(state: &mut OpState) -> Result<Option<u32>, PermissionCheckError> {
395  state
396    .borrow_mut::<PermissionsContainer>()
397    .check_sys("gid", "Deno.gid()")?;
398  Ok(None)
399}
400
401#[cfg(not(windows))]
402#[op2(stack_trace)]
403#[smi]
404fn op_uid(state: &mut OpState) -> Result<Option<u32>, PermissionCheckError> {
405  state
406    .borrow_mut::<PermissionsContainer>()
407    .check_sys("uid", "Deno.uid()")?;
408  // TODO(bartlomieju):
409  #[allow(clippy::undocumented_unsafe_blocks)]
410  unsafe {
411    Ok(Some(libc::getuid()))
412  }
413}
414
415#[cfg(windows)]
416#[op2(stack_trace)]
417#[smi]
418fn op_uid(state: &mut OpState) -> Result<Option<u32>, PermissionCheckError> {
419  state
420    .borrow_mut::<PermissionsContainer>()
421    .check_sys("uid", "Deno.uid()")?;
422  Ok(None)
423}
424
425#[op2]
426#[serde]
427fn op_runtime_cpu_usage() -> (usize, usize) {
428  let (sys, user) = get_cpu_usage();
429  (sys.as_micros() as usize, user.as_micros() as usize)
430}
431
432#[cfg(unix)]
433fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
434  let mut rusage = std::mem::MaybeUninit::uninit();
435
436  // Uses POSIX getrusage from libc
437  // to retrieve user and system times
438  // SAFETY: libc call
439  let ret = unsafe { libc::getrusage(libc::RUSAGE_SELF, rusage.as_mut_ptr()) };
440  if ret != 0 {
441    return Default::default();
442  }
443
444  // SAFETY: already checked the result
445  let rusage = unsafe { rusage.assume_init() };
446
447  let sys = std::time::Duration::from_micros(rusage.ru_stime.tv_usec as u64)
448    + std::time::Duration::from_secs(rusage.ru_stime.tv_sec as u64);
449  let user = std::time::Duration::from_micros(rusage.ru_utime.tv_usec as u64)
450    + std::time::Duration::from_secs(rusage.ru_utime.tv_sec as u64);
451
452  (sys, user)
453}
454
455#[cfg(windows)]
456fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
457  use winapi::shared::minwindef::FALSE;
458  use winapi::shared::minwindef::FILETIME;
459  use winapi::shared::minwindef::TRUE;
460  use winapi::um::minwinbase::SYSTEMTIME;
461  use winapi::um::processthreadsapi::GetCurrentProcess;
462  use winapi::um::processthreadsapi::GetProcessTimes;
463  use winapi::um::timezoneapi::FileTimeToSystemTime;
464
465  fn convert_system_time(system_time: SYSTEMTIME) -> std::time::Duration {
466    std::time::Duration::from_secs(
467      system_time.wHour as u64 * 3600
468        + system_time.wMinute as u64 * 60
469        + system_time.wSecond as u64,
470    ) + std::time::Duration::from_millis(system_time.wMilliseconds as u64)
471  }
472
473  let mut creation_time = std::mem::MaybeUninit::<FILETIME>::uninit();
474  let mut exit_time = std::mem::MaybeUninit::<FILETIME>::uninit();
475  let mut kernel_time = std::mem::MaybeUninit::<FILETIME>::uninit();
476  let mut user_time = std::mem::MaybeUninit::<FILETIME>::uninit();
477
478  // SAFETY: winapi calls
479  let ret = unsafe {
480    GetProcessTimes(
481      GetCurrentProcess(),
482      creation_time.as_mut_ptr(),
483      exit_time.as_mut_ptr(),
484      kernel_time.as_mut_ptr(),
485      user_time.as_mut_ptr(),
486    )
487  };
488
489  if ret != TRUE {
490    return std::default::Default::default();
491  }
492
493  let mut kernel_system_time = std::mem::MaybeUninit::<SYSTEMTIME>::uninit();
494  let mut user_system_time = std::mem::MaybeUninit::<SYSTEMTIME>::uninit();
495
496  // SAFETY: convert to system time
497  unsafe {
498    let sys_ret = FileTimeToSystemTime(
499      kernel_time.assume_init_mut(),
500      kernel_system_time.as_mut_ptr(),
501    );
502    let user_ret = FileTimeToSystemTime(
503      user_time.assume_init_mut(),
504      user_system_time.as_mut_ptr(),
505    );
506
507    match (sys_ret, user_ret) {
508      (TRUE, TRUE) => (
509        convert_system_time(kernel_system_time.assume_init()),
510        convert_system_time(user_system_time.assume_init()),
511      ),
512      (TRUE, FALSE) => (
513        convert_system_time(kernel_system_time.assume_init()),
514        Default::default(),
515      ),
516      (FALSE, TRUE) => (
517        Default::default(),
518        convert_system_time(user_system_time.assume_init()),
519      ),
520      (_, _) => Default::default(),
521    }
522  }
523}
524
525#[cfg(not(any(windows, unix)))]
526fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
527  Default::default()
528}
529
530#[op2]
531#[serde]
532fn op_runtime_memory_usage(
533  scope: &mut v8::HandleScope,
534) -> (usize, usize, usize, usize) {
535  let s = scope.get_heap_statistics();
536
537  let (rss, heap_total, heap_used, external) = (
538    rss(),
539    s.total_heap_size(),
540    s.used_heap_size(),
541    s.external_memory(),
542  );
543
544  (rss, heap_total, heap_used, external)
545}
546
547#[cfg(any(target_os = "android", target_os = "linux"))]
548fn rss() -> usize {
549  // Inspired by https://github.com/Arc-blroth/memory-stats/blob/5364d0d09143de2a470d33161b2330914228fde9/src/linux.rs
550
551  // Extracts a positive integer from a string that
552  // may contain leading spaces and trailing chars.
553  // Returns the extracted number and the index of
554  // the next character in the string.
555  fn scan_int(string: &str) -> (usize, usize) {
556    let mut out = 0;
557    let mut idx = 0;
558    let mut chars = string.chars().peekable();
559    while let Some(' ') = chars.next_if_eq(&' ') {
560      idx += 1;
561    }
562    for n in chars {
563      idx += 1;
564      if n.is_ascii_digit() {
565        out *= 10;
566        out += n as usize - '0' as usize;
567      } else {
568        break;
569      }
570    }
571    (out, idx)
572  }
573
574  #[allow(clippy::disallowed_methods)]
575  let statm_content = if let Ok(c) = std::fs::read_to_string("/proc/self/statm")
576  {
577    c
578  } else {
579    return 0;
580  };
581
582  // statm returns the virtual size and rss, in
583  // multiples of the page size, as the first
584  // two columns of output.
585  // SAFETY: libc call
586  let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
587
588  if page_size < 0 {
589    return 0;
590  }
591
592  let (_total_size_pages, idx) = scan_int(&statm_content);
593  let (total_rss_pages, _) = scan_int(&statm_content[idx..]);
594
595  total_rss_pages * page_size as usize
596}
597
598#[cfg(target_os = "macos")]
599fn rss() -> usize {
600  // Inspired by https://github.com/Arc-blroth/memory-stats/blob/5364d0d09143de2a470d33161b2330914228fde9/src/darwin.rs
601
602  let mut task_info =
603    std::mem::MaybeUninit::<libc::mach_task_basic_info_data_t>::uninit();
604  let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
605  // SAFETY: libc calls
606  let r = unsafe {
607    extern "C" {
608      static mut mach_task_self_: std::ffi::c_uint;
609    }
610    libc::task_info(
611      mach_task_self_,
612      libc::MACH_TASK_BASIC_INFO,
613      task_info.as_mut_ptr() as libc::task_info_t,
614      &mut count as *mut libc::mach_msg_type_number_t,
615    )
616  };
617  // According to libuv this should never fail
618  assert_eq!(r, libc::KERN_SUCCESS);
619  // SAFETY: we just asserted that it was success
620  let task_info = unsafe { task_info.assume_init() };
621  task_info.resident_size as usize
622}
623
624#[cfg(target_os = "openbsd")]
625fn rss() -> usize {
626  // Uses OpenBSD's KERN_PROC_PID sysctl(2)
627  // to retrieve information about the current
628  // process, part of which is the RSS (p_vm_rssize)
629
630  // SAFETY: libc call (get PID of own process)
631  let pid = unsafe { libc::getpid() };
632  // SAFETY: libc call (get system page size)
633  let pagesize = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
634  // KERN_PROC_PID returns a struct libc::kinfo_proc
635  let mut kinfoproc = std::mem::MaybeUninit::<libc::kinfo_proc>::uninit();
636  let mut size = std::mem::size_of_val(&kinfoproc) as libc::size_t;
637  let mut mib = [
638    libc::CTL_KERN,
639    libc::KERN_PROC,
640    libc::KERN_PROC_PID,
641    pid,
642    // mib is an array of integers, size is of type size_t
643    // conversion is safe, because the size of a libc::kinfo_proc
644    // structure will not exceed i32::MAX
645    size.try_into().unwrap(),
646    1,
647  ];
648  // SAFETY: libc call, mib has been statically initialized,
649  // kinfoproc is a valid pointer to a libc::kinfo_proc struct
650  let res = unsafe {
651    libc::sysctl(
652      mib.as_mut_ptr(),
653      mib.len() as _,
654      kinfoproc.as_mut_ptr() as *mut libc::c_void,
655      &mut size,
656      std::ptr::null_mut(),
657      0,
658    )
659  };
660
661  if res == 0 {
662    // SAFETY: sysctl returns 0 on success and kinfoproc is initialized
663    // p_vm_rssize contains size in pages -> multiply with pagesize to
664    // get size in bytes.
665    pagesize * unsafe { (*kinfoproc.as_mut_ptr()).p_vm_rssize as usize }
666  } else {
667    0
668  }
669}
670
671#[cfg(windows)]
672fn rss() -> usize {
673  use winapi::shared::minwindef::DWORD;
674  use winapi::shared::minwindef::FALSE;
675  use winapi::um::processthreadsapi::GetCurrentProcess;
676  use winapi::um::psapi::GetProcessMemoryInfo;
677  use winapi::um::psapi::PROCESS_MEMORY_COUNTERS;
678
679  // SAFETY: winapi calls
680  unsafe {
681    // this handle is a constant—no need to close it
682    let current_process = GetCurrentProcess();
683    let mut pmc: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
684
685    if GetProcessMemoryInfo(
686      current_process,
687      &mut pmc,
688      std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as DWORD,
689    ) != FALSE
690    {
691      pmc.WorkingSetSize
692    } else {
693      0
694    }
695  }
696}
697
698fn os_uptime(state: &mut OpState) -> Result<u64, PermissionCheckError> {
699  state
700    .borrow_mut::<PermissionsContainer>()
701    .check_sys("osUptime", "Deno.osUptime()")?;
702  Ok(sys_info::os_uptime())
703}
704
705#[op2(fast, stack_trace)]
706#[number]
707fn op_os_uptime(state: &mut OpState) -> Result<u64, PermissionCheckError> {
708  os_uptime(state)
709}