Skip to content

Commit 6afc136

Browse files
Claude Botclaude
andcommitted
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>
1 parent cc3fc5a commit 6afc136

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

src/bun.js/node/node_process.zig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ pub fn getTitle(_: *JSGlobalObject, title: *bun.String) callconv(.c) void {
2121
title.* = bun.String.cloneUTF8(str orelse "bun");
2222
}
2323

24-
// TODO: https://github.com/nodejs/node/blob/master/deps/uv/src/unix/darwin-proctitle.c
2524
pub fn setTitle(globalObject: *JSGlobalObject, newvalue: *bun.String) callconv(.c) void {
2625
defer newvalue.deref();
2726
title_mutex.lock();
@@ -34,6 +33,12 @@ pub fn setTitle(globalObject: *JSGlobalObject, newvalue: *bun.String) callconv(.
3433

3534
if (bun.cli.Bun__Node__ProcessTitle) |slice| bun.default_allocator.free(slice);
3635
bun.cli.Bun__Node__ProcessTitle = new_title;
36+
37+
// On Linux, update the process title in /proc/self/cmdline so that
38+
// tools like `ps` and `top` can see it.
39+
if (comptime bun.Environment.isLinux) {
40+
bun.process_title_info.setTitle(new_title);
41+
}
3742
}
3843

3944
pub fn createArgv0(globalObject: *jsc.JSGlobalObject) callconv(.c) jsc.JSValue {

src/bun.zig

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,40 @@ pub const StatFS = switch (Environment.os) {
19161916

19171917
pub var argv: [][:0]const u8 = &[_][:0]const u8{};
19181918

1919+
/// Original argv buffer for process title on Linux.
1920+
/// This stores the original argv memory location so we can overwrite it
1921+
/// when setting process.title, which is how `ps` and other tools see the title.
1922+
pub const ProcessTitleInfo = struct {
1923+
/// Pointer to the start of the original argv buffer
1924+
buffer: ?[*]u8 = null,
1925+
/// Total capacity of the argv buffer (all args plus the null terminators)
1926+
capacity: usize = 0,
1927+
1928+
pub fn setTitle(self: *ProcessTitleInfo, title: []const u8) void {
1929+
if (comptime !Environment.isLinux) return;
1930+
1931+
const buf = self.buffer orelse return;
1932+
const cap = self.capacity;
1933+
if (cap == 0) return;
1934+
1935+
// Copy the title into the original argv buffer
1936+
const title_len = @min(title.len, cap - 1);
1937+
@memcpy(buf[0..title_len], title[0..title_len]);
1938+
// Zero out the rest of the buffer
1939+
@memset(buf[title_len..cap], 0);
1940+
1941+
// Also set the thread name via prctl (limited to 16 bytes including null)
1942+
// This is what shows up in `top` and `htop` under the thread name column
1943+
var name_buf: [16]u8 = undefined;
1944+
const name_len = @min(title.len, 15);
1945+
@memcpy(name_buf[0..name_len], title[0..name_len]);
1946+
name_buf[name_len] = 0;
1947+
_ = std.posix.prctl(.SET_NAME, .{@intFromPtr(&name_buf)}) catch {};
1948+
}
1949+
};
1950+
1951+
pub var process_title_info: ProcessTitleInfo = .{};
1952+
19191953
pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const u8), allocator: std.mem.Allocator) !void {
19201954
var i: usize = 0;
19211955
var offset_in_args: usize = 1;
@@ -2032,6 +2066,23 @@ pub fn initArgv(allocator: std.mem.Allocator) !void {
20322066
for (0..argv.len) |i| {
20332067
argv[i] = std.mem.sliceTo(std.os.argv[i], 0);
20342068
}
2069+
2070+
// On Linux, store the original argv buffer location so we can
2071+
// overwrite it when setting process.title. This is how tools
2072+
// like `ps` see the process title.
2073+
if (comptime Environment.isLinux) {
2074+
if (std.os.argv.len > 0) {
2075+
const first_arg = std.os.argv[0];
2076+
const last_arg = std.os.argv[std.os.argv.len - 1];
2077+
// Calculate the total capacity: from first arg to end of last arg
2078+
const last_arg_slice = std.mem.sliceTo(last_arg, 0);
2079+
const capacity = @intFromPtr(last_arg) - @intFromPtr(first_arg) + last_arg_slice.len + 1;
2080+
process_title_info = .{
2081+
.buffer = @ptrCast(first_arg),
2082+
.capacity = capacity,
2083+
};
2084+
}
2085+
}
20352086
} else if (comptime Environment.isWindows) {
20362087
// Zig's implementation of `std.process.argsAlloc()`on Windows platforms
20372088
// is not reliable, specifically the way it splits the command line string.

test/js/node/process/process.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ it("process.title with UTF-16 characters", () => {
102102
expect(process.title).toBe("bun");
103103
});
104104

105+
it.skipIf(!process.platform.startsWith("linux"))(
106+
"process.title is visible in /proc/self/cmdline on Linux",
107+
async () => {
108+
// On Linux, process.title should be visible via /proc/self/cmdline
109+
// This is how tools like `ps` and `top` see the process title
110+
const fs = await import("fs");
111+
112+
const testTitle = "bun-test-title-12345";
113+
process.title = testTitle;
114+
expect(process.title).toBe(testTitle);
115+
116+
// Read /proc/self/cmdline to verify the title is set
117+
const cmdline = fs.readFileSync("/proc/self/cmdline", "utf8");
118+
expect(cmdline).toContain(testTitle);
119+
120+
// Reset title
121+
process.title = "bun";
122+
},
123+
);
124+
105125
it("process.chdir() on root dir", () => {
106126
const cwd = process.cwd();
107127
try {

0 commit comments

Comments
 (0)