From aa8078b6888ee4d55ef348e336e076676dffc25f Mon Sep 17 00:00:00 2001 From: Leo Kettmeir Date: Mon, 31 Jul 2023 22:29:09 +0200 Subject: [PATCH] feat(node/os): implement getPriority, setPriority & userInfo (#19370) Takes #4202 over Closes #17850 --------- Co-authored-by: ecyrbe --- Cargo.lock | 14 ++ cli/tests/integration/shared_library_tests.rs | 4 +- cli/tests/unit_node/os_test.ts | 54 +++-- ext/node/Cargo.toml | 4 + ext/node/lib.rs | 7 + ext/node/ops/mod.rs | 1 + ext/node/ops/os.rs | 188 ++++++++++++++++++ ext/node/polyfills/internal/errors.ts | 13 ++ ext/node/polyfills/os.ts | 49 ++++- runtime/build.rs | 7 + runtime/permissions/mod.rs | 4 + 11 files changed, 314 insertions(+), 31 deletions(-) create mode 100644 ext/node/ops/os.rs diff --git a/Cargo.lock b/Cargo.lock index 7ba53ae628..e736e86ca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,11 +1296,13 @@ dependencies = [ "dsa", "ecb", "elliptic-curve 0.13.4", + "errno", "hex", "hkdf", "idna 0.3.0", "indexmap 1.9.2", "lazy-regex", + "libc", "libz-sys", "md-5", "md4", @@ -1328,6 +1330,8 @@ dependencies = [ "signature 1.6.4", "tokio", "typenum", + "whoami", + "winapi", "x25519-dalek", "x509-parser", ] @@ -6089,6 +6093,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "whoami" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "widestring" version = "0.5.1" diff --git a/cli/tests/integration/shared_library_tests.rs b/cli/tests/integration/shared_library_tests.rs index 531fdd5169..641deab4c1 100644 --- a/cli/tests/integration/shared_library_tests.rs +++ b/cli/tests/integration/shared_library_tests.rs @@ -45,12 +45,14 @@ fn macos_shared_libraries() { // target/release/deno: // /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1953.255.0) // /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 1228.0.0) + // /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration (compatibility version 1.0.0, current version 1241.100.11) // /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 60420.60.24) // /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0) // /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0) - const EXPECTED: [&str; 6] = + const EXPECTED: [&str; 7] = ["/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation", "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices", + "/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration", "/System/Library/Frameworks/Security.framework/Versions/A/Security", "/usr/lib/libiconv.2.dylib", "/usr/lib/libSystem.B.dylib", diff --git a/cli/tests/unit_node/os_test.ts b/cli/tests/unit_node/os_test.ts index 85164d1e60..2d0d3a8c80 100644 --- a/cli/tests/unit_node/os_test.ts +++ b/cli/tests/unit_node/os_test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import { assert, assertEquals, + assertNotEquals, assertThrows, } from "../../../test_util/std/testing/asserts.ts"; @@ -252,28 +253,39 @@ Deno.test({ }); Deno.test({ - name: "APIs not yet implemented", + name: "os.setPriority() & os.getPriority()", + // disabled because os.getPriority() doesn't work without sudo + ignore: true, fn() { - assertThrows( - () => { - os.getPriority(); - }, - Error, - "Not implemented", - ); - assertThrows( - () => { - os.setPriority(0); - }, - Error, - "Not implemented", - ); - assertThrows( - () => { - os.userInfo(); - }, - Error, - "Not implemented", + const child = new Deno.Command(Deno.execPath(), { + args: ["eval", "while (true) { console.log('foo') }"], + }).spawn(); + const originalPriority = os.getPriority(child.pid); + assertNotEquals(originalPriority, os.constants.priority.PRIORITY_HIGH); + os.setPriority(child.pid, os.constants.priority.PRIORITY_HIGH); + assertEquals( + os.getPriority(child.pid), + os.constants.priority.PRIORITY_HIGH, ); + os.setPriority(child.pid, originalPriority); + assertEquals(os.getPriority(child.pid), originalPriority); + child.kill(); + }, +}); + +Deno.test({ + name: + "os.setPriority() throw os permission denied error & os.getPriority() doesn't", + async fn() { + const child = new Deno.Command(Deno.execPath(), { + args: ["eval", "while (true) { console.log('foo') }"], + }).spawn(); + assertThrows( + () => os.setPriority(child.pid, os.constants.priority.PRIORITY_HIGH), + Deno.errors.PermissionDenied, + ); + os.getPriority(child.pid); + child.kill(); + await child.status; }, }); diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 0c0beda650..834493d5a3 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -28,11 +28,13 @@ digest = { version = "0.10.5", features = ["core-api", "std"] } dsa = "0.6.1" ecb.workspace = true elliptic-curve.workspace = true +errno = "0.2.8" hex.workspace = true hkdf.workspace = true idna = "0.3.0" indexmap.workspace = true lazy-regex.workspace = true +libc.workspace = true libz-sys = { version = "1.1.8", features = ["static"] } md-5 = "0.10.5" md4 = "0.10.2" @@ -60,6 +62,8 @@ sha2.workspace = true signature.workspace = true tokio.workspace = true typenum = "1.15.0" +whoami = "1.4.0" +winapi.workspace = true # https://github.com/dalek-cryptography/x25519-dalek/pull/89 x25519-dalek = "2.0.0-pre.1" x509-parser = "0.15.0" diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 8f910ac63f..318de77e1c 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -53,6 +53,7 @@ pub trait NodePermissions { api_name: &str, ) -> Result<(), AnyError>; fn check_read(&self, path: &Path) -> Result<(), AnyError>; + fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError>; } pub(crate) struct AllowAllNodePermissions; @@ -68,6 +69,9 @@ impl NodePermissions for AllowAllNodePermissions { fn check_read(&self, _path: &Path) -> Result<(), AnyError> { Ok(()) } + fn check_sys(&self, _kind: &str, _api_name: &str) -> Result<(), AnyError> { + Ok(()) + } } #[allow(clippy::disallowed_types)] @@ -243,6 +247,9 @@ deno_core::extension!(deno_node, ops::zlib::brotli::op_brotli_decompress_stream, ops::zlib::brotli::op_brotli_decompress_stream_end, ops::http::op_node_http_request

, + ops::os::op_node_os_get_priority

, + ops::os::op_node_os_set_priority

, + ops::os::op_node_os_username

, op_node_build_os, op_is_any_arraybuffer, op_node_is_promise_rejected, diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index 2bbf02d343..22ad546e89 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -3,6 +3,7 @@ pub mod crypto; pub mod http; pub mod idna; +pub mod os; pub mod require; pub mod v8; pub mod winerror; diff --git a/ext/node/ops/os.rs b/ext/node/ops/os.rs new file mode 100644 index 0000000000..0a841a72ac --- /dev/null +++ b/ext/node/ops/os.rs @@ -0,0 +1,188 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use crate::NodePermissions; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::OpState; +use errno::errno; +use errno::set_errno; +use errno::Errno; + +#[op] +pub fn op_node_os_get_priority

( + state: &mut OpState, + pid: u32, +) -> Result +where + P: NodePermissions + 'static, +{ + { + let permissions = state.borrow_mut::

(); + permissions.check_sys("getPriority", "node:os.getPriority()")?; + } + + priority::get_priority(pid) +} + +#[op] +pub fn op_node_os_set_priority

( + state: &mut OpState, + pid: u32, + priority: i32, +) -> Result<(), AnyError> +where + P: NodePermissions + 'static, +{ + { + let permissions = state.borrow_mut::

(); + permissions.check_sys("setPriority", "node:os.setPriority()")?; + } + + priority::set_priority(pid, priority) +} + +#[op] +pub fn op_node_os_username

(state: &mut OpState) -> Result +where + P: NodePermissions + 'static, +{ + { + let permissions = state.borrow_mut::

(); + permissions.check_sys("userInfo", "node:os.userInfo()")?; + } + + Ok(whoami::username()) +} + +const PRIORITY_HIGH: i32 = -14; + +#[cfg(unix)] +mod priority { + use super::*; + use libc::id_t; + use libc::PRIO_PROCESS; + + #[cfg(target_os = "macos")] + #[allow(non_camel_case_types)] + type priority_t = i32; + #[cfg(target_os = "linux")] + #[allow(non_camel_case_types)] + type priority_t = u32; + + // Ref: https://github.com/libuv/libuv/blob/55376b044b74db40772e8a6e24d67a8673998e02/src/unix/core.c#L1533-L1547 + pub fn get_priority(pid: u32) -> Result { + set_errno(Errno(0)); + match ( + // SAFETY: libc::getpriority is unsafe + unsafe { libc::getpriority(PRIO_PROCESS as priority_t, pid as id_t) }, + errno(), + ) { + (-1, Errno(0)) => Ok(PRIORITY_HIGH), + (-1, _) => Err(std::io::Error::last_os_error().into()), + (priority, _) => Ok(priority), + } + } + + pub fn set_priority(pid: u32, priority: i32) -> Result<(), AnyError> { + // SAFETY: libc::setpriority is unsafe + match unsafe { + libc::setpriority(PRIO_PROCESS as priority_t, pid as id_t, priority) + } { + -1 => Err(std::io::Error::last_os_error().into()), + _ => Ok(()), + } + } +} + +#[cfg(windows)] +mod priority { + use super::*; + use deno_core::error::type_error; + use winapi::shared::minwindef::DWORD; + use winapi::shared::minwindef::FALSE; + use winapi::shared::ntdef::NULL; + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::GetCurrentProcess; + use winapi::um::processthreadsapi::GetPriorityClass; + use winapi::um::processthreadsapi::OpenProcess; + use winapi::um::processthreadsapi::SetPriorityClass; + use winapi::um::winbase::ABOVE_NORMAL_PRIORITY_CLASS; + use winapi::um::winbase::BELOW_NORMAL_PRIORITY_CLASS; + use winapi::um::winbase::HIGH_PRIORITY_CLASS; + use winapi::um::winbase::IDLE_PRIORITY_CLASS; + use winapi::um::winbase::NORMAL_PRIORITY_CLASS; + use winapi::um::winbase::REALTIME_PRIORITY_CLASS; + use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION; + + // Taken from: https://github.com/libuv/libuv/blob/a877ca2435134ef86315326ef4ef0c16bdbabf17/include/uv.h#L1318-L1323 + const PRIORITY_LOW: i32 = 19; + const PRIORITY_BELOW_NORMAL: i32 = 10; + const PRIORITY_NORMAL: i32 = 0; + const PRIORITY_ABOVE_NORMAL: i32 = -7; + const PRIORITY_HIGHEST: i32 = -20; + + // Ported from: https://github.com/libuv/libuv/blob/a877ca2435134ef86315326ef4ef0c16bdbabf17/src/win/util.c#L1649-L1685 + pub fn get_priority(pid: u32) -> Result { + unsafe { + let handle = if pid == 0 { + GetCurrentProcess() + } else { + OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid as DWORD) + }; + if handle == NULL { + Err(std::io::Error::last_os_error().into()) + } else { + let result = match GetPriorityClass(handle) { + 0 => Err(std::io::Error::last_os_error().into()), + REALTIME_PRIORITY_CLASS => Ok(PRIORITY_HIGHEST), + HIGH_PRIORITY_CLASS => Ok(PRIORITY_HIGH), + ABOVE_NORMAL_PRIORITY_CLASS => Ok(PRIORITY_ABOVE_NORMAL), + NORMAL_PRIORITY_CLASS => Ok(PRIORITY_NORMAL), + BELOW_NORMAL_PRIORITY_CLASS => Ok(PRIORITY_BELOW_NORMAL), + IDLE_PRIORITY_CLASS => Ok(PRIORITY_LOW), + _ => Ok(PRIORITY_LOW), + }; + CloseHandle(handle); + result + } + } + } + + // Ported from: https://github.com/libuv/libuv/blob/a877ca2435134ef86315326ef4ef0c16bdbabf17/src/win/util.c#L1688-L1719 + pub fn set_priority(pid: u32, priority: i32) -> Result<(), AnyError> { + unsafe { + let handle = if pid == 0 { + GetCurrentProcess() + } else { + OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid as DWORD) + }; + if handle == NULL { + Err(std::io::Error::last_os_error().into()) + } else { + let priority_class = + if priority < PRIORITY_HIGHEST || priority > PRIORITY_LOW { + return Err(type_error("Invalid priority")); + } else if priority < PRIORITY_HIGH { + REALTIME_PRIORITY_CLASS + } else if priority < PRIORITY_ABOVE_NORMAL { + HIGH_PRIORITY_CLASS + } else if priority < PRIORITY_NORMAL { + ABOVE_NORMAL_PRIORITY_CLASS + } else if priority < PRIORITY_BELOW_NORMAL { + NORMAL_PRIORITY_CLASS + } else if priority < PRIORITY_LOW { + BELOW_NORMAL_PRIORITY_CLASS + } else { + IDLE_PRIORITY_CLASS + }; + + let result = match SetPriorityClass(handle, priority_class) { + FALSE => Err(std::io::Error::last_os_error().into()), + _ => Ok(()), + }; + CloseHandle(handle); + result + } + } + } +} diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index 55098d79bc..2ba7ec28e4 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -2497,6 +2497,19 @@ export class ERR_FS_RMDIR_ENOTDIR extends NodeSystemError { } } +export class ERR_OS_NO_HOMEDIR extends NodeSystemError { + constructor() { + const code = isWindows ? "ENOENT" : "ENOTDIR"; + const ctx: NodeSystemErrorCtx = { + message: "not a directory", + syscall: "home", + code, + errno: isWindows ? osConstants.errno.ENOENT : osConstants.errno.ENOTDIR, + }; + super(code, ctx, "Path is not a directory"); + } +} + interface UvExceptionContext { syscall: string; path?: string; diff --git a/ext/node/polyfills/os.ts b/ext/node/polyfills/os.ts index acdc1c9770..a874c942cb 100644 --- a/ext/node/polyfills/os.ts +++ b/ext/node/polyfills/os.ts @@ -23,15 +23,16 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { notImplemented } from "ext:deno_node/_utils.ts"; +const core = globalThis.__bootstrap.core; import { validateIntegerRange } from "ext:deno_node/_utils.ts"; import process from "node:process"; import { isWindows, osType } from "ext:deno_node/_util/os.ts"; +import { ERR_OS_NO_HOMEDIR } from "ext:deno_node/internal/errors.ts"; import { os } from "ext:deno_node/internal_binding/constants.ts"; import { osUptime } from "ext:runtime/30_os.js"; -export const constants = os; +import { Buffer } from "ext:deno_node/internal/buffer.mjs"; -const SEE_GITHUB_ISSUE = "See https://github.com/denoland/deno_std/issues/1436"; +export const constants = os; interface CPUTimes { /** The number of milliseconds the CPU has spent in user mode */ @@ -93,8 +94,8 @@ interface UserInfo { username: string; uid: number; gid: number; - shell: string; - homedir: string; + shell: string | null; + homedir: string | null; } export function arch(): string { @@ -161,7 +162,7 @@ export function freemem(): number { /** Not yet implemented */ export function getPriority(pid = 0): number { validateIntegerRange(pid, "pid"); - notImplemented(SEE_GITHUB_ISSUE); + return core.ops.op_node_os_get_priority(pid); } /** Returns the string path of the current user's home directory. */ @@ -257,7 +258,7 @@ export function setPriority(pid: number, priority?: number) { validateIntegerRange(pid, "pid"); validateIntegerRange(priority, "priority", -20, 19); - notImplemented(SEE_GITHUB_ISSUE); + core.ops.op_node_os_set_priority(pid, priority); } /** Returns the operating system's default directory for temporary files as a string. */ @@ -317,10 +318,40 @@ export function uptime(): number { /** Not yet implemented */ export function userInfo( - // deno-lint-ignore no-unused-vars options: UserInfoOptions = { encoding: "utf-8" }, ): UserInfo { - notImplemented(SEE_GITHUB_ISSUE); + const uid = Deno.uid(); + const gid = Deno.gid(); + + if (isWindows) { + uid = -1; + gid = -1; + } + + // TODO(@crowlKats): figure out how to do this correctly: + // The value of homedir returned by os.userInfo() is provided by the operating system. + // This differs from the result of os.homedir(), which queries environment + // variables for the home directory before falling back to the operating system response. + let _homedir = homedir(); + if (!_homedir) { + throw new ERR_OS_NO_HOMEDIR(); + } + let shell = isWindows ? (Deno.env.get("SHELL") || null) : null; + let username = core.ops.op_node_os_username(); + + if (options?.encoding === "buffer") { + _homedir = _homedir ? Buffer.from(_homedir) : _homedir; + shell = shell ? Buffer.from(shell) : shell; + username = Buffer.from(username); + } + + return { + uid, + gid, + homedir: _homedir, + shell, + username, + }; } export const EOL = isWindows ? "\r\n" : "\n"; diff --git a/runtime/build.rs b/runtime/build.rs index d7de298832..828bc3c536 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -139,6 +139,13 @@ mod startup_snapshot { fn check_read(&self, _p: &Path) -> Result<(), deno_core::error::AnyError> { unreachable!("snapshotting!") } + fn check_sys( + &self, + _kind: &str, + _api_name: &str, + ) -> Result<(), deno_core::error::AnyError> { + unreachable!("snapshotting!") + } } impl deno_net::NetPermissions for Permissions { diff --git a/runtime/permissions/mod.rs b/runtime/permissions/mod.rs index 84fcfa6aa0..56ebf83273 100644 --- a/runtime/permissions/mod.rs +++ b/runtime/permissions/mod.rs @@ -1886,6 +1886,10 @@ impl deno_node::NodePermissions for PermissionsContainer { fn check_read(&self, path: &Path) -> Result<(), AnyError> { self.0.lock().read.check(path, None) } + + fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> { + self.0.lock().sys.check(kind, Some(api_name)) + } } impl deno_net::NetPermissions for PermissionsContainer {