mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## What does this PR do? Fixes https://github.com/oven-sh/bun/issues/22650 Fixes https://github.com/oven-sh/bun/issues/22615 Fixes https://github.com/oven-sh/bun/issues/22603 Fixes https://github.com/oven-sh/bun/issues/22602 Fixes a crash that occurred when running shell commands through `bun run` (package.json scripts) on Windows that use the `&&` operator followed by an external command. ### The Problem The minimal reproduction was: ```bash bun exec 'echo && node --version' ``` This would crash with: `panic(main thread): attempt to use null value` ### Root Causes Two issues were causing the crash: 1. **Missing top_level_dir**: When `runPackageScriptForeground` creates a MiniEventLoop for running package scripts, it wasn't setting the `top_level_dir` field. This caused a null pointer dereference when the shell tried to access it. 2. **MovableIfWindowsFd handling**: After PR #21800 introduced `MovableIfWindowsFd` to handle file descriptor ownership on Windows, the `IOWriter.fd` could be moved to libuv, leaving it null. When the shell tried to spawn an external command after a `&&` operator, it would crash trying to access this null fd. ### The Fix 1. Set `mini.top_level_dir = cwd` after initializing the MiniEventLoop in `run_command.zig` 2. In `IO.zig`, when the fd has been moved to libuv (is null), use `.inherit` for stdio instead of trying to pass the null fd ### How did you verify your code works? - Added a regression test that reproduces the issue - Verified the test fails without the fix and passes with it - Tested the minimal reproduction command directly - The fix correctly allows both commands in the `&&` chain to execute ```bash # Before fix: crashes > bun exec 'echo test && node --version' panic(main thread): attempt to use null value # After fix: works correctly > bun exec 'echo test && node --version' test v22.4.1 ``` <sub> also probably fixes #22615 and fixes #22603 and fixes #22602 </sub> --------- Co-authored-by: Zack Radisic <zack@theradisic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
449 lines
14 KiB
Zig
449 lines
14 KiB
Zig
pub const opener = switch (@import("builtin").target.os.tag) {
|
|
.macos => "/usr/bin/open",
|
|
.windows => "start",
|
|
else => "xdg-open",
|
|
};
|
|
|
|
fn fallback(url: string) void {
|
|
Output.prettyln("-> {s}", .{url});
|
|
Output.flush();
|
|
}
|
|
|
|
pub fn openURL(url: stringZ) void {
|
|
if (comptime Environment.isWasi) return fallback(url);
|
|
|
|
var args_buf = [_]stringZ{ opener, url };
|
|
|
|
maybe_fallback: {
|
|
switch (bun.spawnSync(&.{
|
|
.argv = &args_buf,
|
|
|
|
.envp = null,
|
|
|
|
.stderr = .inherit,
|
|
.stdout = .inherit,
|
|
.stdin = .inherit,
|
|
|
|
.windows = if (Environment.isWindows) .{
|
|
.loop = bun.jsc.EventLoopHandle.init(bun.jsc.MiniEventLoop.initGlobal(null, null)),
|
|
},
|
|
}) catch break :maybe_fallback) {
|
|
// don't fallback:
|
|
.result => |*result| if (result.isOK()) return,
|
|
.err => {},
|
|
}
|
|
}
|
|
|
|
fallback(url);
|
|
}
|
|
|
|
pub const Editor = enum(u8) {
|
|
none,
|
|
sublime,
|
|
vscode,
|
|
atom,
|
|
textmate,
|
|
intellij,
|
|
webstorm,
|
|
vim,
|
|
neovim,
|
|
emacs,
|
|
other,
|
|
|
|
const StringMap = std.EnumMap(Editor, string);
|
|
const StringArrayMap = std.EnumMap(Editor, []const [:0]const u8);
|
|
|
|
const name_map = bun.ComptimeStringMap(Editor, .{
|
|
.{ "sublime", .sublime },
|
|
.{ "subl", .sublime },
|
|
.{ "vscode", .vscode },
|
|
.{ "code", .vscode },
|
|
.{ "textmate", .textmate },
|
|
.{ "mate", .textmate },
|
|
.{ "atom", .atom },
|
|
.{ "idea", .intellij },
|
|
.{ "webstorm", .webstorm },
|
|
.{ "nvim", .neovim },
|
|
.{ "neovim", .neovim },
|
|
.{ "vim", .vim },
|
|
.{ "vi", .vim },
|
|
.{ "emacs", .emacs },
|
|
});
|
|
|
|
pub fn byName(name: string) ?Editor {
|
|
if (strings.indexOfChar(name, ' ')) |i| {
|
|
return name_map.get(name[0..i]);
|
|
}
|
|
|
|
return name_map.get(name);
|
|
}
|
|
|
|
pub fn detect(env: *DotEnv.Loader) ?Editor {
|
|
const vars = .{ "EDITOR", "VISUAL" };
|
|
inline for (vars) |name| {
|
|
if (env.get(name)) |value| {
|
|
const basename = std.fs.path.basename(value);
|
|
if (byName(basename)) |editor| {
|
|
return editor;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const which = @import("./which.zig").which;
|
|
pub fn byPATH(env: *DotEnv.Loader, buf: *bun.PathBuffer, cwd: string, out: *[]const u8) ?Editor {
|
|
const PATH = env.get("PATH") orelse return null;
|
|
|
|
inline for (default_preference_list) |editor| {
|
|
if (bin_name.get(editor)) |path| {
|
|
if (which(buf, PATH, cwd, path)) |bin| {
|
|
out.* = bun.asByteSlice(bin);
|
|
return editor;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn byPATHForEditor(env: *DotEnv.Loader, editor: Editor, buf: *bun.PathBuffer, cwd: string, out: *[]const u8) bool {
|
|
const PATH = env.get("PATH") orelse return false;
|
|
|
|
if (bin_name.get(editor)) |path| {
|
|
if (path.len > 0) {
|
|
if (which(buf, PATH, cwd, path)) |bin| {
|
|
out.* = bun.asByteSlice(bin);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn byFallbackPathForEditor(editor: Editor, out: ?*[]const u8) bool {
|
|
if (bin_path.get(editor)) |paths| {
|
|
for (paths) |path| {
|
|
if (std.fs.cwd().openFile(path, .{})) |opened| {
|
|
opened.close();
|
|
if (out != null) {
|
|
out.?.* = bun.asByteSlice(path);
|
|
}
|
|
return true;
|
|
} else |_| {}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
pub fn byFallback(env: *DotEnv.Loader, buf: *bun.PathBuffer, cwd: string, out: *[]const u8) ?Editor {
|
|
inline for (default_preference_list) |editor| {
|
|
if (byPATHForEditor(env, editor, buf, cwd, out)) {
|
|
return editor;
|
|
}
|
|
|
|
if (byFallbackPathForEditor(editor, out)) {
|
|
return editor;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub const default_preference_list = [_]Editor{
|
|
.vscode,
|
|
.sublime,
|
|
.atom,
|
|
.neovim,
|
|
|
|
.webstorm,
|
|
.intellij,
|
|
.textmate,
|
|
.vim,
|
|
};
|
|
|
|
pub const bin_name: StringMap = brk: {
|
|
var map = StringMap{};
|
|
|
|
map.put(.sublime, "subl");
|
|
map.put(.vscode, "code");
|
|
map.put(.atom, "atom");
|
|
map.put(.textmate, "mate");
|
|
map.put(.intellij, "idea");
|
|
map.put(.webstorm, "webstorm");
|
|
map.put(.vim, "vim");
|
|
map.put(.neovim, "nvim");
|
|
map.put(.emacs, "emacs");
|
|
map.put(.other, "");
|
|
break :brk map;
|
|
};
|
|
|
|
pub const bin_path: StringArrayMap = brk: {
|
|
var map = StringArrayMap{};
|
|
|
|
if (Environment.isMac) {
|
|
map.put(.vscode, &.{
|
|
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
|
|
"/Applications/VSCodium.app/Contents/Resources/app/bin/code",
|
|
});
|
|
map.put(
|
|
.atom,
|
|
&.{
|
|
"/Applications/Atom.app/Contents/Resources/app/atom.sh",
|
|
},
|
|
);
|
|
map.put(
|
|
.sublime,
|
|
&.{
|
|
"/Applications/Sublime Text 4.app/Contents/SharedSupport/bin/subl",
|
|
"/Applications/Sublime Text 3.app/Contents/SharedSupport/bin/subl",
|
|
"/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl",
|
|
"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl",
|
|
},
|
|
);
|
|
}
|
|
|
|
break :brk map;
|
|
};
|
|
|
|
pub fn isJetBrains(editor: Editor) bool {
|
|
return switch (editor) {
|
|
.intellij, .webstorm => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
pub fn open(
|
|
editor: Editor,
|
|
binary: string,
|
|
file: []const u8,
|
|
line: ?string,
|
|
column: ?string,
|
|
_: std.mem.Allocator,
|
|
) !void {
|
|
var spawned = try default_allocator.create(SpawnedEditorContext);
|
|
spawned.* = .{};
|
|
var file_path_buf_stream = std.io.fixedBufferStream(&spawned.file_path_buf);
|
|
var file_path_buf_writer = file_path_buf_stream.writer();
|
|
var args_buf = &spawned.buf;
|
|
errdefer default_allocator.destroy(spawned);
|
|
|
|
var i: usize = 0;
|
|
|
|
if (editor == .vim or editor == .emacs or editor == .neovim) {
|
|
args_buf[0] = opener;
|
|
i += 1;
|
|
|
|
args_buf[i] = binary;
|
|
i += 1;
|
|
|
|
if (Environment.isMac) {
|
|
args_buf[i] = "--args";
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
args_buf[i] = binary;
|
|
i += 1;
|
|
|
|
if (editor == .vscode and line != null and line.?.len > 0) {
|
|
args_buf[i] = "--goto";
|
|
|
|
i += 1;
|
|
}
|
|
|
|
switch (editor) {
|
|
.sublime, .atom, .vscode, .webstorm, .intellij => {
|
|
try file_path_buf_writer.writeAll(file);
|
|
if (line) |line_| {
|
|
if (line_.len > 0) {
|
|
try file_path_buf_writer.print(":{s}", .{line_});
|
|
|
|
if (!editor.isJetBrains()) {
|
|
if (column) |col| {
|
|
if (col.len > 0)
|
|
try file_path_buf_writer.print(":{s}", .{col});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (file_path_buf_stream.pos > 0) {
|
|
args_buf[i] = file_path_buf_stream.getWritten();
|
|
i += 1;
|
|
}
|
|
},
|
|
.textmate => {
|
|
try file_path_buf_writer.writeAll(file);
|
|
const file_path = file_path_buf_stream.getWritten();
|
|
|
|
if (line) |line_| {
|
|
if (line_.len > 0) {
|
|
args_buf[i] = "--line";
|
|
i += 1;
|
|
|
|
try file_path_buf_writer.print("{s}", .{line_});
|
|
|
|
if (column) |col| {
|
|
if (col.len > 0)
|
|
try file_path_buf_writer.print(":{s}", .{col});
|
|
}
|
|
|
|
const line_column = file_path_buf_stream.getWritten()[file_path.len..];
|
|
if (line_column.len > 0) {
|
|
args_buf[i] = line_column;
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (file_path_buf_stream.pos > 0) {
|
|
args_buf[i] = file_path;
|
|
i += 1;
|
|
}
|
|
},
|
|
else => {
|
|
if (file.len > 0) {
|
|
try file_path_buf_writer.writeAll(file);
|
|
const file_path = file_path_buf_stream.getWritten();
|
|
args_buf[i] = file_path;
|
|
i += 1;
|
|
}
|
|
},
|
|
}
|
|
|
|
spawned.child_process = std.process.Child.init(args_buf[0..i], default_allocator);
|
|
var thread = try std.Thread.spawn(.{}, autoClose, .{spawned});
|
|
thread.detach();
|
|
}
|
|
const SpawnedEditorContext = struct {
|
|
file_path_buf: [1024 + bun.MAX_PATH_BYTES]u8 = undefined,
|
|
buf: [10]string = undefined,
|
|
child_process: std.process.Child = undefined,
|
|
};
|
|
|
|
fn autoClose(spawned: *SpawnedEditorContext) void {
|
|
defer bun.default_allocator.destroy(spawned);
|
|
|
|
Global.setThreadName("Open Editor");
|
|
spawned.child_process.spawn() catch return;
|
|
_ = spawned.child_process.wait() catch {};
|
|
}
|
|
};
|
|
|
|
pub const EditorContext = struct {
|
|
editor: ?Editor = null,
|
|
name: string = "",
|
|
path: string = "",
|
|
const Fs = @import("./fs.zig");
|
|
|
|
pub fn openInEditor(this: *EditorContext, editor_: Editor, blob: []const u8, id: string, tmpdir: std.fs.Dir, line: string, column: string) void {
|
|
_openInEditor(this.path, editor_, blob, id, tmpdir, line, column) catch |err| {
|
|
if (editor_ != .other) {
|
|
Output.prettyErrorln("Error {s} opening in {s}", .{ @errorName(err), @tagName(editor_) });
|
|
}
|
|
|
|
this.editor = Editor.none;
|
|
};
|
|
}
|
|
|
|
fn _openInEditor(path: string, editor_: Editor, blob: []const u8, id: string, tmpdir: std.fs.Dir, line: string, column: string) !void {
|
|
var basename_buf: [512]u8 = undefined;
|
|
var basename = std.fs.path.basename(id);
|
|
if (strings.endsWith(basename, ".bun") and basename.len < 499) {
|
|
bun.copy(u8, &basename_buf, basename);
|
|
basename_buf[basename.len..][0..3].* = ".js".*;
|
|
basename = basename_buf[0 .. basename.len + 3];
|
|
}
|
|
|
|
try tmpdir.writeFile(basename, blob);
|
|
|
|
var opened = try tmpdir.openFile(basename, .{});
|
|
defer opened.close();
|
|
var path_buf: bun.PathBuffer = undefined;
|
|
try editor_.open(
|
|
path,
|
|
try bun.getFdPath(opened.handle, &path_buf),
|
|
line,
|
|
column,
|
|
default_allocator,
|
|
);
|
|
}
|
|
|
|
pub fn autoDetectEditor(this: *EditorContext, env: *DotEnv.Loader) void {
|
|
if (this.editor == null) {
|
|
this.detectEditor(env);
|
|
}
|
|
}
|
|
pub fn detectEditor(this: *EditorContext, env: *DotEnv.Loader) void {
|
|
var buf: bun.PathBuffer = undefined;
|
|
|
|
var out: string = "";
|
|
// first: choose from user preference
|
|
if (this.name.len > 0) {
|
|
// /usr/bin/vim
|
|
if (std.fs.path.isAbsolute(this.name)) {
|
|
this.editor = Editor.byName(std.fs.path.basename(this.name)) orelse Editor.other;
|
|
this.path = this.name;
|
|
return;
|
|
}
|
|
|
|
// "vscode"
|
|
if (Editor.byName(std.fs.path.basename(this.name))) |editor_| {
|
|
if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) {
|
|
this.editor = editor_;
|
|
this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable;
|
|
return;
|
|
}
|
|
|
|
// not in path, try common ones
|
|
if (Editor.byFallbackPathForEditor(editor_, &out)) {
|
|
this.editor = editor_;
|
|
this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// EDITOR=code
|
|
if (Editor.detect(env)) |editor_| {
|
|
if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) {
|
|
this.editor = editor_;
|
|
this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable;
|
|
return;
|
|
}
|
|
|
|
// not in path, try common ones
|
|
if (Editor.byFallbackPathForEditor(editor_, &out)) {
|
|
this.editor = editor_;
|
|
this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Don't know, so we will just guess based on what exists
|
|
if (Editor.byFallback(env, &buf, Fs.FileSystem.instance.top_level_dir, &out)) |editor_| {
|
|
this.editor = editor_;
|
|
this.path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable;
|
|
return;
|
|
}
|
|
|
|
this.editor = Editor.none;
|
|
}
|
|
};
|
|
|
|
const string = []const u8;
|
|
const stringZ = [:0]const u8;
|
|
|
|
const DotEnv = @import("./env_loader.zig");
|
|
const std = @import("std");
|
|
|
|
const bun = @import("bun");
|
|
const Environment = bun.Environment;
|
|
const Global = bun.Global;
|
|
const Output = bun.Output;
|
|
const default_allocator = bun.default_allocator;
|
|
const strings = bun.strings;
|