Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ pub fn open_in_editor(global_this: &JSGlobalObject, callframe: &CallFrame) -> Js
edit.name =
unsafe { bun_ptr::detach_lifetime(slot.name_storage.as_slice()) };
edit.detect_editor(env);
editor_choice = edit.editor;
editor_choice = edit.editor.filter(|e| *e != Editor::None);
if editor_choice.is_none() {
slot.name_storage = prev_storage;
*edit = prev;
Expand Down Expand Up @@ -1074,11 +1074,11 @@ pub fn open_in_editor(global_this: &JSGlobalObject, callframe: &CallFrame) -> Js
}
}

let editor = match editor_choice.or(edit.editor) {
let editor = match editor_choice.or(edit.editor).filter(|e| *e != Editor::None) {
Some(e) => e,
None => {
edit.auto_detect_editor(env);
match edit.editor {
match edit.editor.filter(|e| *e != Editor::None) {
Some(e) => e,
None => {
return Err(global_this.throw(format_args!("Failed to auto-detect editor")));
Expand Down
53 changes: 53 additions & 0 deletions test/js/bun/util/open-in-editor-gc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,59 @@ import { bunEnv, bunExe, isLinux, tempDir } from "harness";
import { existsSync, symlinkSync } from "node:fs";
import { join } from "node:path";

// When no editor can be detected (no bunfig editor, no EDITOR/VISUAL, nothing
// in PATH), Bun.openInEditor must throw instead of spawning a detached editor
// thread with an empty argv[0]. Repeated calls used to fork a doomed child per
// call, which let fuzzed scripts create thousands of threads/processes and
// stress the GC/scavenger thread-suspension signal until the process died.
test.skipIf(!isLinux)("Bun.openInEditor throws when no editor can be found", async () => {
using dir = tempDir("open-in-editor-none", {
"empty-path/.keep": "",
});

const env: Record<string, string | undefined> = {
...bunEnv,
PATH: join(String(dir), "empty-path"),
};
delete env.EDITOR;
delete env.VISUAL;

await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
function f0(a1, a2, a3) {}
let threw = 0;
let silent = 0;
let lastError = "";
for (let i = 0; i < 50; i++) {
try {
Bun.openInEditor(i % 2 ? "foo.js" : f0);
silent++;
} catch (e) {
threw++;
lastError = e.message;
}
}
Bun.gc(true);
console.log(JSON.stringify({ threw, silent, lastError }));
`,
],
env,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ threw: 50, silent: 0, lastError: "Failed to auto-detect editor" });
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(0);
});

// On Linux, JSC uses SIGPWR to suspend/resume threads for GC and the libpas
// scavenger. Bun.openInEditor spawns a detached thread that goes through
// bun.spawnSync, whose signal-forwarding setup must not touch SIGPWR or the
Expand Down
Loading