Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ deno_core::extension!(deno_node,
ops::ipc::op_node_ipc_buffer_constructor,
ops::ipc::op_node_ipc_ref,
ops::ipc::op_node_ipc_unref,
ops::process::op_node_process_set_title,
ops::process::op_node_process_kill,
ops::process::op_node_process_setegid,
ops::process::op_node_process_seteuid,
Expand Down
153 changes: 153 additions & 0 deletions ext/node/ops/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,159 @@ use nix::unistd::User;

use crate::ExtNodeSys;

// --- process.title support ---

#[cfg(target_os = "linux")]
mod argv_store {
use std::ffi::c_char;
use std::ffi::c_int;
use std::sync::Once;

static mut ARGV_PTR: *mut *mut c_char = std::ptr::null_mut();
static mut ARGV_BUF_SIZE: usize = 0;
static INIT: Once = Once::new();

/// # Safety
/// Called from `.init_array` before main. Must only store the pointers.
pub unsafe fn save(argc: c_int, argv: *mut *mut c_char) {
INIT.call_once(|| {
if argv.is_null() || argc <= 0 {
return;
}
let argc = argc as usize;
// SAFETY: argv is valid and has argc entries (guaranteed by the OS loader).
unsafe {
// Calculate the contiguous buffer size from argv[0] to end of argv[argc-1]
let start = (*argv) as *const u8;
let last_arg = *argv.add(argc - 1);
let last_arg_len = libc::strlen(last_arg);
let end = last_arg.add(last_arg_len + 1) as *const u8;
let buf_size = end.offset_from(start) as usize;

ARGV_PTR = argv;
ARGV_BUF_SIZE = buf_size;
}
});
}

/// # Safety
/// The stored argv pointer must still be valid (it always is for the process lifetime).
pub unsafe fn overwrite(title: &str) {
// SAFETY: ARGV_PTR and ARGV_BUF_SIZE are set once in save() and remain
// valid for the process lifetime. The buffer at *ARGV_PTR is the original
// argv[0] area allocated by the OS.
unsafe {
if ARGV_PTR.is_null() || ARGV_BUF_SIZE == 0 {
return;
}
let buf =
std::slice::from_raw_parts_mut(*ARGV_PTR as *mut u8, ARGV_BUF_SIZE);
let title_bytes = title.as_bytes();
let copy_len = title_bytes.len().min(ARGV_BUF_SIZE - 1);
buf[..copy_len].copy_from_slice(&title_bytes[..copy_len]);
buf[copy_len..].fill(0);
}
}
}

#[cfg(target_os = "linux")]
#[used]
#[unsafe(link_section = ".init_array")]
static ARGV_INIT: unsafe extern "C" fn(
libc::c_int,
*mut *mut libc::c_char,
*mut *mut libc::c_char,
) = {
unsafe extern "C" fn init(
argc: libc::c_int,
argv: *mut *mut libc::c_char,
_envp: *mut *mut libc::c_char,
) {
// SAFETY: argc and argv are provided by the OS at process init and are valid.
unsafe { argv_store::save(argc, argv) };
}
init
};

#[cfg(target_os = "macos")]
fn set_process_title(title: &str) {
// SAFETY: We call macOS-specific C functions to read and overwrite the
// process argv buffer in place. The argv pointer and argc count come from
// the OS and are valid for the lifetime of the process. We bounds-check
// before writing and null-terminate the buffer.
unsafe {
unsafe extern "C" {
fn _NSGetArgc() -> *mut libc::c_int;
fn _NSGetArgv() -> *mut *mut *mut libc::c_char;
}

let argc = *_NSGetArgc() as usize;
let argv = *_NSGetArgv();
if argv.is_null() || argc == 0 {
return;
}

// Calculate contiguous buffer size from argv[0] through argv[argc-1]
let start = *argv as *const u8;
let last_arg = *argv.add(argc - 1);
let last_arg_len = libc::strlen(last_arg);
let end = last_arg.add(last_arg_len + 1) as *const u8;
let buf_size = end.offset_from(start) as usize;

// Overwrite argv[0] buffer with the new title
let buf = std::slice::from_raw_parts_mut(*argv as *mut u8, buf_size);
let title_bytes = title.as_bytes();
let copy_len = title_bytes.len().min(buf_size - 1);
buf[..copy_len].copy_from_slice(&title_bytes[..copy_len]);
buf[copy_len..].fill(0);

// Also set the pthread name (visible in Activity Monitor / debugger, 63 char limit)
let c_title =
std::ffi::CString::new(&title.as_bytes()[..title.len().min(63)]);
if let Ok(c_title) = c_title {
libc::pthread_setname_np(c_title.as_ptr());
}
}
}

#[cfg(target_os = "linux")]
fn set_process_title(title: &str) {
// SAFETY: We overwrite the saved argv buffer with the new title via
// argv_store, then call prctl to set the kernel thread name.
unsafe {
argv_store::overwrite(title);

// Also set the kernel thread name via prctl (15 char limit, visible in /proc/self/comm)
let truncated = &title.as_bytes()[..title.len().min(15)];
if let Ok(c_title) = std::ffi::CString::new(truncated) {
libc::prctl(libc::PR_SET_NAME, c_title.as_ptr() as libc::c_ulong);
}
}
}

#[cfg(target_os = "windows")]
fn set_process_title(title: &str) {
let wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
// SAFETY: FFI call, wide is null-terminated
unsafe {
winapi::um::wincon::SetConsoleTitleW(wide.as_ptr());
}
}

#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
)))]
fn set_process_title(_title: &str) {
// No-op on unsupported platforms
}

#[op2(fast)]
pub fn op_node_process_set_title(#[string] title: &str) {
set_process_title(title);
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum ProcessError {
#[class(inherit)]
Expand Down
7 changes: 7 additions & 0 deletions ext/node/polyfills/internal_binding/node_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const {
SafeMap,
ArrayPrototypeForEach,
SafeRegExp,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;

// This module ports:
Expand All @@ -20,13 +22,18 @@ export function getOptions() {
const options = new SafeMap([
["--warnings", { value: true }],
["--pending-deprecation", { value: false }],
["--title", { value: "" }],
]);

const nodeOptions = Deno.env.get("NODE_OPTIONS");
const args = nodeOptions
? StringPrototypeSplit(nodeOptions, new SafeRegExp("\\s"))
: [];
ArrayPrototypeForEach(args, (arg) => {
if (StringPrototypeStartsWith(arg, "--title=")) {
options.set("--title", { value: StringPrototypeSlice(arg, 8) });
return;
}
switch (arg) {
case "--no-warnings":
options.set("--warnings", { value: false });
Expand Down
19 changes: 14 additions & 5 deletions ext/node/polyfills/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
op_node_load_env_file,
op_node_process_constrained_memory,
op_node_process_kill,
op_node_process_set_title,
op_node_process_setegid,
op_node_process_seteuid,
op_node_process_setgid,
Expand Down Expand Up @@ -620,14 +621,17 @@ Object.defineProperty(process, "report", {
},
});

let processTitle: string | undefined;
Object.defineProperty(process, "title", {
get() {
return "deno";
if (processTitle == null) {
return String(execPath);
}
return processTitle;
},
set(_value) {
// NOTE(bartlomieju): this is a noop. Node.js doesn't guarantee that the
// process name will be properly set and visible from other tools anyway.
// Might revisit in the future.
set(value) {
processTitle = `${value}`;
op_node_process_set_title(processTitle);
},
});

Expand Down Expand Up @@ -1170,6 +1174,11 @@ internals.__bootstrapNodeProcess = function (
execPath = Deno.execPath();
initializeDebugEnv(nodeDebug);

const title = getOptionValue("--title");
if (title) {
process.title = title;
}

if (getOptionValue("--warnings")) {
process.on("warning", onWarning);
}
Expand Down
3 changes: 3 additions & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,7 @@
"parallel/test-process-no-deprecation.js": {},
"parallel/test-process-ppid.js": {},
"parallel/test-process-really-exit.js": {},
"parallel/test-process-title-cli.js": {},
"parallel/test-process-release.js": {},
"parallel/test-process-umask-mask.js": {},
"parallel/test-process-umask.js": {},
Expand Down Expand Up @@ -1729,6 +1730,7 @@
"parallel/test-runner-subtest-after-hook.js": {},
// TODO(bartlomieju): disabled during work on `node:inspector`, this test didn't actualy run before
// "parallel/test-set-process-debug-port.js": {},
"parallel/test-setproctitle.js": {},
"parallel/test-signal-handler-remove-on-exit.js": {},
"parallel/test-signal-handler.js": {},
"parallel/test-snapshot-api.js": {
Expand Down Expand Up @@ -2545,6 +2547,7 @@
"sequential/test-net-GH-5504.js": {},
"sequential/test-net-response-size.js": {},
"sequential/test-net-server-bind.js": {},
"sequential/test-process-title.js": {},
"sequential/test-require-cache-without-stat.js": {},
"sequential/test-resolution-inspect-brk.js": {},
// TODO(bartlomieju): disabled during work on `node:inspector`, this test didn't actualy run before
Expand Down
3 changes: 3 additions & 0 deletions tests/node_compat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ fn parse_flags(source: &str) -> (Vec<String>, Vec<String>) {
"--allow-natives-syntax" => {
v8_flags.push("--allow-natives-syntax".to_string());
}
f if f.starts_with("--title=") => {
node_options.push(f.to_string());
}
_ => {}
}
}
Expand Down
10 changes: 7 additions & 3 deletions tests/unit_node/process_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,10 +1077,14 @@ Deno.test({
Deno.test({
name: "process.title",
fn() {
assertEquals(process.title, "deno");
// Verify that setting the value has no effect.
// Default process.title should be the execPath (matches Node.js behavior)
assertEquals(process.title, process.execPath);
// Setting process.title should work
const original = process.title;
process.title = "foo";
assertEquals(process.title, "deno");
assertEquals(process.title, "foo");
// Restore
process.title = original;
},
});

Expand Down
Loading