diff --git a/Cargo.lock b/Cargo.lock index 16b00365a3cf6d..4d3c0ebe7e4cea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,7 @@ dependencies = [ "dirs", "dlopen", "dprint-plugin-typescript", + "errno", "futures 0.3.4", "fwdansi", "glob", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0115938f7bd8fa..a49d9b044db31b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -34,6 +34,7 @@ clap = "2.33.0" dirs = "2.0.2" dlopen = "0.1.8" dprint-plugin-typescript = "0.7.0" +errno = "0.1.8" futures = { version = "0.3.1", features = [ "compat", "io-compat" ] } glob = "0.3.0" http = "0.2.0" diff --git a/cli/js/deno.ts b/cli/js/deno.ts index c563d5112cb19a..ddad910958d0fd 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -93,6 +93,9 @@ export { execPath, hostname, loadavg, + OsPriority, + getPriority, + setPriority, osRelease } from "./os.ts"; export { diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index b2e67b2889e54d..aa0fb0aed503c2 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -71,6 +71,42 @@ declare namespace Deno { /** Exit the Deno process with optional exit code. */ export function exit(code?: number): never; + /** **UNSTABLE**: Might not use all-caps. */ + export enum OsPriority { + LOW = 19, + BELOW_NORMAL = 10, + NORMAL = 0, + ABOVE_NORMAL = -7, + HIGH = -14, + HIGHEST = -20 + } + + /** + * **UNSTABLE:** new api + * + * Returns the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the priority of the current process is returned. + */ + export function getPriority(pid?: number): number; + + /** + * **UNSTABLE:** new api + * + * Attempts to set the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the process ID of the current process is used. + * The priority input must be an integer between -20 (high priority) and 19 (low priority). + * Due to differences between Unix priority levels and Windows priority classes, + * priority is mapped to one of six priority constants in Deno.OsPriority enum. + * When retrieving a process priority level, this range mapping may cause the return value to be slightly different on Windows. + * To avoid confusion, set priority to one of the priority constants. + * On Windows, setting priority to Deno.OsPriority.HIGHEST requires elevated user privileges. + * Otherwise the set priority will be silently reduced to Deno.OsPriority.HIGH. + */ + export function setPriority( + priority: number | OsPriority, + pid?: number + ): void; + /** Returns a snapshot of the environment variables at invocation. Mutating a * property in the object will set that variable in the environment for the * process. The environment object will only accept `string`s as values. diff --git a/cli/js/os.ts b/cli/js/os.ts index 89632e34fc7611..538570412f227e 100644 --- a/cli/js/os.ts +++ b/cli/js/os.ts @@ -36,6 +36,43 @@ export function exit(code = 0): never { return util.unreachable(); } +/** **UNSTABLE**: Might not use all-caps. */ +export enum OsPriority { + LOW = 19, + BELOW_NORMAL = 10, + NORMAL = 0, + ABOVE_NORMAL = -7, + HIGH = -14, + HIGHEST = -20 +} + +/** + * **UNSTABLE:** new api + * + * Returns the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the priority of the current process is returned. + */ +export function getPriority(pid = 0): number { + return sendSync("op_get_priority", { pid }); +} + +/** + * **UNSTABLE:** new api + * + * Attempts to set the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the process ID of the current process is used. + * The priority input must be an integer between -20 (high priority) and 19 (low priority). + * Due to differences between Unix priority levels and Windows priority classes, + * priority is mapped to one of six priority constants in Deno.OsPriority enum. + * When retrieving a process priority level, this range mapping may cause the return value to be slightly different on Windows. + * To avoid confusion, set priority to one of the priority constants. + * On Windows, setting priority to Deno.OsPriority.HIGHEST requires elevated user privileges. + * Otherwise the set priority will be silently reduced to Deno.OsPriority.HIGH. + */ +export function setPriority(priority: number | OsPriority, pid = 0): void { + sendSync("op_set_priority", { pid, priority }); +} + function setEnv(key: string, value: string): void { sendSync("op_set_env", { key, value }); } diff --git a/cli/js/os_test.ts b/cli/js/os_test.ts index b0561840bb69fb..2ad0a7d74dce0a 100644 --- a/cli/js/os_test.ts +++ b/cli/js/os_test.ts @@ -116,6 +116,39 @@ test(function osPid(): void { assert(Deno.pid > 0); }); +test(function testGetPriorityShouldBeBetweenBounds() { + const priority = Deno.getPriority(); + assert(Deno.OsPriority.HIGHEST <= priority); + assert(priority <= Deno.OsPriority.LOW); +}); + +if (Deno.build.os === "win") + test(function testSetPriorityShouldBeSetToHigh() { + Deno.setPriority(Deno.OsPriority.HIGH); + assertEquals(Deno.getPriority(), Deno.OsPriority.HIGH); + }); + +if (Deno.build.os === "win") + test(function testSetPriorityShouldBeSetToAboveNormal() { + Deno.setPriority(Deno.OsPriority.ABOVE_NORMAL); + assertEquals(Deno.getPriority(), Deno.OsPriority.ABOVE_NORMAL); + }); + +test(function testSetPriorityShouldBeSetToNormal() { + Deno.setPriority(Deno.OsPriority.NORMAL); + assertEquals(Deno.getPriority(), Deno.OsPriority.NORMAL); +}); + +test(function testSetPriorityShouldBeSetToBelowNormal() { + Deno.setPriority(Deno.OsPriority.BELOW_NORMAL); + assertEquals(Deno.getPriority(), Deno.OsPriority.BELOW_NORMAL); +}); + +test(function testSetPriorityShouldBeSetToLow() { + Deno.setPriority(Deno.OsPriority.LOW); + assertEquals(Deno.getPriority(), Deno.OsPriority.LOW); +}); + testPerm({ env: true }, function getDir(): void { type supportOS = "mac" | "win" | "linux"; diff --git a/cli/lib.rs b/cli/lib.rs index a4d4ec331deac5..b292eefdad9188 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -43,6 +43,7 @@ pub mod msg; pub mod op_error; pub mod ops; pub mod permissions; +pub mod priority; mod repl; pub mod resolve_addr; pub mod signal; diff --git a/cli/ops/os.rs b/cli/ops/os.rs index 428856707821e1..473163afdc0232 100644 --- a/cli/ops/os.rs +++ b/cli/ops/os.rs @@ -1,6 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::dispatch_json::{Deserialize, JsonOp, Value}; use crate::op_error::OpError; +use crate::priority::{get_priority, set_priority}; use crate::state::State; use deno_core::*; use std::collections::HashMap; @@ -19,6 +20,8 @@ pub fn init(i: &mut Isolate, s: &State) { i.register_op("op_hostname", s.stateful_json_op(op_hostname)); i.register_op("op_loadavg", s.stateful_json_op(op_loadavg)); i.register_op("op_os_release", s.stateful_json_op(op_os_release)); + i.register_op("op_get_priority", s.stateful_json_op(op_get_priority)); + i.register_op("op_set_priority", s.stateful_json_op(op_set_priority)); } #[derive(Deserialize)] @@ -185,3 +188,34 @@ fn op_os_release( let release = sys_info::os_release().unwrap_or_else(|_| "".to_string()); Ok(JsonOp::Sync(json!(release))) } + +#[derive(Deserialize)] +struct GetPriorityArgs { + pid: u32, +} + +fn op_get_priority( + _state: &State, + args: Value, + _zero_copy: Option, +) -> Result { + let args: GetPriorityArgs = serde_json::from_value(args)?; + let priority = get_priority(args.pid)?; + Ok(JsonOp::Sync(json!(priority))) +} + +#[derive(Deserialize)] +struct SetPriorityArgs { + pid: u32, + priority: i32, +} + +fn op_set_priority( + _state: &State, + args: Value, + _zero_copy: Option, +) -> Result { + let args: SetPriorityArgs = serde_json::from_value(args)?; + set_priority(args.pid, args.priority)?; + Ok(JsonOp::Sync(json!({}))) +} diff --git a/cli/priority.rs b/cli/priority.rs new file mode 100644 index 00000000000000..ac8f409974dd05 --- /dev/null +++ b/cli/priority.rs @@ -0,0 +1,167 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +//! This module provides cross-platform ability to get and set program scheduling priority. +//! It uses libc getpriority/setpriority on posix +//! It uses winapi GetPriorityClass/SetPriorityClass on windows +use crate::op_error::OpError; + +#[cfg(unix)] +use errno::{errno, set_errno, Errno}; +#[cfg(unix)] +use libc::{id_t, PRIO_PROCESS}; +#[cfg(windows)] +use winapi::shared::minwindef::{DWORD, FALSE}; +#[cfg(windows)] +use winapi::shared::ntdef::NULL; +#[cfg(windows)] +use winapi::um::handleapi::CloseHandle; +#[cfg(windows)] +use winapi::um::processthreadsapi::{ + GetCurrentProcess, GetPriorityClass, OpenProcess, SetPriorityClass, +}; +#[cfg(windows)] +use winapi::um::winbase::{ + ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, + HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, + REALTIME_PRIORITY_CLASS, +}; +#[cfg(windows)] +use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION; +#[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; + +pub const PRIORITY_LOW: i32 = 19; +pub const PRIORITY_BELOW_NORMAL: i32 = 10; +pub const PRIORITY_NORMAL: i32 = 0; +pub const PRIORITY_ABOVE_NORMAL: i32 = -7; +pub const PRIORITY_HIGH: i32 = -14; +pub const PRIORITY_HIGHEST: i32 = -20; + +/* **UNSTABLE:** new api */ +#[cfg(unix)] +pub fn get_priority(pid: u32) -> Result { + unsafe { + set_errno(Errno(0)); + match ( + libc::getpriority(PRIO_PROCESS as priority_t, pid as id_t), + errno(), + ) { + (-1, Errno(0)) => Ok(PRIORITY_HIGH), + (-1, _) => Err(OpError::from(std::io::Error::last_os_error())), + (priority, _) => Ok(priority), + } + } +} + +/* **UNSTABLE:** new api */ +#[cfg(unix)] +pub fn set_priority(pid: u32, priority: i32) -> Result<(), OpError> { + unsafe { + match libc::setpriority(PRIO_PROCESS as priority_t, pid as id_t, priority) { + -1 => Err(OpError::from(std::io::Error::last_os_error())), + _ => Ok(()), + } + } +} + +/* **UNSTABLE:** new api */ +#[cfg(windows)] +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(OpError::from(std::io::Error::last_os_error())) + } else { + let result = match GetPriorityClass(handle) { + 0 => Err(OpError::from(std::io::Error::last_os_error())), + 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 + } + } +} + +/* **UNSTABLE:** new api */ +#[cfg(windows)] +pub fn set_priority(pid: u32, priority: i32) -> Result<(), OpError> { + unsafe { + let handle = if pid == 0 { + GetCurrentProcess() + } else { + OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid as DWORD) + }; + if handle == NULL { + Err(OpError::from(std::io::Error::last_os_error())) + } else { + let prio_class = match priority { + p if p <= PRIORITY_HIGHEST => REALTIME_PRIORITY_CLASS, + p if PRIORITY_HIGHEST < p && p <= PRIORITY_HIGH => HIGH_PRIORITY_CLASS, + p if PRIORITY_HIGH < p && p <= PRIORITY_ABOVE_NORMAL => { + ABOVE_NORMAL_PRIORITY_CLASS + } + p if PRIORITY_ABOVE_NORMAL < p && p <= PRIORITY_NORMAL => { + NORMAL_PRIORITY_CLASS + } + p if PRIORITY_NORMAL < p && p <= PRIORITY_BELOW_NORMAL => { + BELOW_NORMAL_PRIORITY_CLASS + } + _ => IDLE_PRIORITY_CLASS, + }; + let result = match SetPriorityClass(handle, prio_class) { + FALSE => Err(OpError::from(std::io::Error::last_os_error())), + _ => Ok(()), + }; + CloseHandle(handle); + result + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_current_process_priority() { + get_priority(0).expect("Should get priority"); + } + + #[cfg(unix)] + #[test] + fn test_set_current_process_high_priority_should_fail() { + assert!(set_priority(0, PRIORITY_HIGH).is_err()); + } + + /// this test makes multiple tests at once + /// because we need to set them in order and rust + /// does not guarantee test order execution + #[test] + fn test_set_current_process_priority_from_normal_to_low() { + set_priority(0, PRIORITY_NORMAL).expect("Should set priority"); + let priority = get_priority(0).expect("Should get priority"); + assert_eq!(priority, PRIORITY_NORMAL); + + set_priority(0, PRIORITY_BELOW_NORMAL).expect("Should set priority"); + let priority = get_priority(0).expect("Should get priority"); + assert_eq!(priority, PRIORITY_BELOW_NORMAL); + + set_priority(0, PRIORITY_LOW).expect("Should set priority"); + let priority = get_priority(0).expect("Should get priority"); + assert_eq!(priority, PRIORITY_LOW); + } +} diff --git a/std/node/os.ts b/std/node/os.ts index 4bc70d1ffa4a01..e625d83d1a7f03 100644 --- a/std/node/os.ts +++ b/std/node/os.ts @@ -128,10 +128,15 @@ export function freemem(): number { notImplemented(SEE_GITHUB_ISSUE); } -/** Not yet implemented */ +/** + * **UNSTABLE:** new api + * + * Returns the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the priority of the current process is returned. + */ export function getPriority(pid = 0): number { validateIntegerRange(pid, "pid"); - notImplemented(SEE_GITHUB_ISSUE); + return Deno.getPriority(pid); } /** Returns the string path of the current user's home directory. */ @@ -166,7 +171,19 @@ export function release(): string { return Deno.osRelease(); } -/** Not yet implemented */ +/** + * **UNSTABLE:** new api + * + * Attempts to set the scheduling priority for the process specified by pid. + * If pid is not provided or is 0, the process ID of the current process is used. + * The priority input must be an integer between -20 (high priority) and 19 (low priority). + * Due to differences between Unix priority levels and Windows priority classes, + * priority is mapped to one of six priority constants in os.constants.priority. + * When retrieving a process priority level, this range mapping may cause the return value to be slightly different on Windows. + * To avoid confusion, set priority to one of the priority constants. + * On Windows, setting priority to PRIORITY_HIGHEST requires elevated user privileges. + * Otherwise the set priority will be silently reduced to PRIORITY_HIGH. + */ export function setPriority(pid: number, priority?: number): void { /* The node API has the 'pid' as the first parameter and as optional. This makes for a problematic implementation in Typescript. */ @@ -175,9 +192,13 @@ export function setPriority(pid: number, priority?: number): void { pid = 0; } validateIntegerRange(pid, "pid"); - validateIntegerRange(priority, "priority", -20, 19); - - notImplemented(SEE_GITHUB_ISSUE); + validateIntegerRange( + priority, + "priority", + Deno.OsPriority.HIGHEST, + Deno.OsPriority.LOW + ); + Deno.setPriority(priority, pid); } /** Returns the operating system's default directory for temporary files as a string. */ @@ -219,6 +240,12 @@ export const constants = { signals: Deno.Signal, priority: { // see https://nodejs.org/docs/latest-v12.x/api/os.html#os_priority_constants + PRIORITY_LOW: Deno.OsPriority.LOW, + PRIORITY_BELOW_NORMAL: Deno.OsPriority.BELOW_NORMAL, + PRIORITY_NORMAL: Deno.OsPriority.NORMAL, + PRIORITY_ABOVE_NORMAL: Deno.OsPriority.ABOVE_NORMAL, + PRIORITY_HIGH: Deno.OsPriority.HIGH, + PRIORITY_HIGHEST: Deno.OsPriority.HIGHEST } }; diff --git a/std/node/os_test.ts b/std/node/os_test.ts index a73a2d4e99d2e4..ea663ce96a82c8 100644 --- a/std/node/os_test.ts +++ b/std/node/os_test.ts @@ -44,6 +44,60 @@ test({ } }); +test({ + name: "getPriority(): should get current process priority if no params", + fn() { + const priority = os.getPriority(); + assert(os.constants.priority.PRIORITY_HIGHEST <= priority); + assert(priority <= os.constants.priority.PRIORITY_LOW); + } +}); + +if (os.platform() === "win32") + test({ + name: "setPriority(): should set current process priority to high", + fn() { + os.setPriority(os.constants.priority.PRIORITY_HIGH); + assertEquals(os.getPriority(), os.constants.priority.PRIORITY_HIGH); + } + }); + +if (os.platform() === "win32") + test({ + name: "setPriority(): should set current process priority to above normal", + fn() { + os.setPriority(os.constants.priority.PRIORITY_ABOVE_NORMAL); + assertEquals( + os.getPriority(), + os.constants.priority.PRIORITY_ABOVE_NORMAL + ); + } + }); + +test({ + name: "setPriority(): should set current process priority to normal", + fn() { + os.setPriority(os.constants.priority.PRIORITY_NORMAL); + assertEquals(os.getPriority(), os.constants.priority.PRIORITY_NORMAL); + } +}); + +test({ + name: "setPriority(): should set current process priority to below normal", + fn() { + os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL); + assertEquals(os.getPriority(), os.constants.priority.PRIORITY_BELOW_NORMAL); + } +}); + +test({ + name: "setPriority(): should set current process priority to low", + fn() { + os.setPriority(os.constants.priority.PRIORITY_LOW); + assertEquals(os.getPriority(), os.constants.priority.PRIORITY_LOW); + } +}); + test({ name: "getPriority(): PID must be a 32 bit integer", fn() { @@ -216,13 +270,6 @@ test({ Error, "Not implemented" ); - assertThrows( - () => { - os.getPriority(); - }, - Error, - "Not implemented" - ); assertThrows( () => { os.networkInterfaces(); @@ -230,13 +277,6 @@ test({ Error, "Not implemented" ); - assertThrows( - () => { - os.setPriority(0); - }, - Error, - "Not implemented" - ); assertThrows( () => { os.totalmem();