Compare commits

...

6 Commits

Author SHA1 Message Date
Jarred Sumner
746cec1117 Merge branch 'main' into claude/fix-process-title-linux 2025-12-15 17:05:00 -08:00
Claude Bot
5874a79ba5 fix(test): increase timeout for v8 test native module builds
The v8 test compiles native modules with node-gyp in the beforeAll hook,
which takes much longer than the default 5 second timeout. This was
causing the test to time out and fail on slower machines or CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 21:55:48 +00:00
Jarred Sumner
f26bb5bee4 Merge branch 'main' into claude/fix-process-title-linux 2025-12-15 12:03:20 -08:00
Jarred Sumner
d0a536b577 Merge branch 'main' into claude/fix-process-title-linux 2025-12-15 11:13:38 -08:00
Jarred Sumner
c32ee7cc68 Merge branch 'main' into claude/fix-process-title-linux 2025-12-14 20:20:35 -08:00
Claude Bot
6afc136429 fix(process): make process.title work properly on Linux
On Linux, process.title should modify the process name visible in
tools like `ps`, `top`, and `htop`. Previously, setting process.title
only updated an internal variable but did not modify the actual process
title that external tools see.

This fix implements process.title properly on Linux by:
1. Storing the original argv buffer location during initialization
2. Overwriting the original argv memory when process.title is set
   (this is how /proc/self/cmdline is populated)
3. Using prctl(PR_SET_NAME) to set the thread name (shown in top/htop)

This matches Node.js behavior, which uses libuv's implementation that
does the same argv memory overwriting technique.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 00:05:13 +00:00
4 changed files with 80 additions and 2 deletions

View File

@@ -21,7 +21,6 @@ pub fn getTitle(_: *JSGlobalObject, title: *bun.String) callconv(.c) void {
title.* = bun.String.cloneUTF8(str orelse "bun");
}
// TODO: https://github.com/nodejs/node/blob/master/deps/uv/src/unix/darwin-proctitle.c
pub fn setTitle(globalObject: *JSGlobalObject, newvalue: *bun.String) callconv(.c) void {
defer newvalue.deref();
title_mutex.lock();
@@ -34,6 +33,12 @@ pub fn setTitle(globalObject: *JSGlobalObject, newvalue: *bun.String) callconv(.
if (bun.cli.Bun__Node__ProcessTitle) |slice| bun.default_allocator.free(slice);
bun.cli.Bun__Node__ProcessTitle = new_title;
// On Linux, update the process title in /proc/self/cmdline so that
// tools like `ps` and `top` can see it.
if (comptime bun.Environment.isLinux) {
bun.process_title_info.setTitle(new_title);
}
}
pub fn createArgv0(globalObject: *jsc.JSGlobalObject) callconv(.c) jsc.JSValue {

View File

@@ -1925,6 +1925,39 @@ pub const StatFS = switch (Environment.os) {
pub var argv: [][:0]const u8 = &[_][:0]const u8{};
/// Original argv buffer for process title on Linux.
/// This stores the original argv memory location so we can overwrite it
/// when setting process.title, which is how `ps` and other tools see the title.
pub const ProcessTitleInfo = struct {
/// Pointer to the start of the original argv buffer
buffer: ?[*]u8 = null,
/// Total capacity of the argv buffer (all args plus the null terminators)
capacity: usize = 0,
pub fn setTitle(self: *ProcessTitleInfo, title: []const u8) void {
if (comptime !Environment.isLinux) return;
const buf = self.buffer orelse return;
const cap = self.capacity;
if (cap == 0) return;
// Copy the title into the original argv buffer
const title_len = @min(title.len, cap - 1);
@memcpy(buf[0..title_len], title[0..title_len]);
// Zero out the rest of the buffer
@memset(buf[title_len..cap], 0);
// Also set the thread name via prctl (limited to 16 bytes including null)
// This is what shows up in `top` and `htop` under the thread name column
var name_buf: [16]u8 = .{0} ** 16;
const name_len = @min(title.len, 15);
@memcpy(name_buf[0..name_len], title[0..name_len]);
_ = std.posix.prctl(.SET_NAME, .{@intFromPtr(&name_buf)}) catch {};
}
};
pub var process_title_info: ProcessTitleInfo = .{};
pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const u8), allocator: std.mem.Allocator) !void {
var i: usize = 0;
var offset_in_args: usize = 1;
@@ -2041,6 +2074,23 @@ pub fn initArgv(allocator: std.mem.Allocator) !void {
for (0..argv.len) |i| {
argv[i] = std.mem.sliceTo(std.os.argv[i], 0);
}
// On Linux, store the original argv buffer location so we can
// overwrite it when setting process.title. This is how tools
// like `ps` see the process title.
if (comptime Environment.isLinux) {
if (std.os.argv.len > 0) {
const first_arg = std.os.argv[0];
const last_arg = std.os.argv[std.os.argv.len - 1];
// Calculate the total capacity: from first arg to end of last arg
const last_arg_slice = std.mem.sliceTo(last_arg, 0);
const capacity = @intFromPtr(last_arg) - @intFromPtr(first_arg) + last_arg_slice.len + 1;
process_title_info = .{
.buffer = @ptrCast(first_arg),
.capacity = capacity,
};
}
}
} else if (comptime Environment.isWindows) {
// Zig's implementation of `std.process.argsAlloc()`on Windows platforms
// is not reliable, specifically the way it splits the command line string.

View File

@@ -102,6 +102,26 @@ it("process.title with UTF-16 characters", () => {
expect(process.title).toBe("bun");
});
it.skipIf(!process.platform.startsWith("linux"))(
"process.title is visible in /proc/self/cmdline on Linux",
async () => {
// On Linux, process.title should be visible via /proc/self/cmdline
// This is how tools like `ps` and `top` see the process title
const fs = await import("fs");
const testTitle = "bun-test-title-12345";
process.title = testTitle;
expect(process.title).toBe(testTitle);
// Read /proc/self/cmdline to verify the title is set
const cmdline = fs.readFileSync("/proc/self/cmdline", "utf8");
expect(cmdline).toContain(testTitle);
// Reset title
process.title = "bun";
},
);
it("process.chdir() on root dir", () => {
const cwd = process.cwd();
try {

View File

@@ -1,10 +1,13 @@
import { spawn } from "bun";
import { beforeAll, describe, expect, it } from "bun:test";
import { beforeAll, describe, expect, it, setDefaultTimeout } from "bun:test";
import { bunEnv, bunExe, isASAN, isBroken, isMusl, isWindows, nodeExe, tmpdirSync } from "harness";
import assert from "node:assert";
import fs from "node:fs/promises";
import { basename, join } from "path";
// Building native modules with node-gyp takes a long time
setDefaultTimeout(1000 * 60 * 5); // 5 minutes
enum Runtime {
node,
bun,