diff --git a/src/__global.zig b/src/__global.zig index e1aca54628..c4c4bcadbc 100644 --- a/src/__global.zig +++ b/src/__global.zig @@ -96,12 +96,6 @@ pub fn exit(code: u8) noreturn { } pub fn exitWide(code: u32) noreturn { - runExitCallbacks(); - Output.flush(); - std.mem.doNotOptimizeAway(&Bun__atexit); - if (Environment.isWindows) { - bun.windows.libuv.uv_library_shutdown(); - } std.c.exit(@bitCast(code)); } @@ -122,6 +116,9 @@ pub fn raiseIgnoringPanicHandler(sig: anytype) noreturn { std.os.sigaction(@intCast(sig), &act, null) catch {}; } } + + Output.Source.Stdio.restore(); + // TODO(@paperdave): report a bug that this intcast shouldnt be needed. signals are i32 not u32 // after that is fixed we can make this function take i32 _ = std.c.raise(@intCast(sig)); @@ -230,3 +227,19 @@ pub export const Bun__userAgent: [*:0]const u8 = Global.user_agent; comptime { _ = Bun__userAgent; } + +pub export fn Bun__onExit() void { + runExitCallbacks(); + Output.flush(); + std.mem.doNotOptimizeAway(&Bun__atexit); + + Output.Source.Stdio.restore(); + + if (Environment.isWindows) { + bun.windows.libuv.uv_library_shutdown(); + } +} + +comptime { + _ = Bun__onExit; +} diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index 16a44f582d..8246c2870f 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -493,6 +493,10 @@ pub const Status = union(enum) { signaled: bun.SignalCode, err: bun.sys.Error, + pub fn isOK(this: *const Status) bool { + return this.* == .exited and this.exited.code == 0; + } + pub const Exited = struct { code: u8 = 0, signal: bun.SignalCode = @enumFromInt(0), @@ -1719,6 +1723,10 @@ pub const sync = struct { status: Status, stdout: std.ArrayList(u8) = .{ .items = &.{}, .allocator = bun.default_allocator, .capacity = 0 }, stderr: std.ArrayList(u8) = .{ .items = &.{}, .allocator = bun.default_allocator, .capacity = 0 }, + + pub fn isOK(this: *const Result) bool { + return this.status.isOK(); + } }; const SyncWindowsPipeReader = struct { diff --git a/src/bun.js/bindings/bun-spawn.cpp b/src/bun.js/bindings/bun-spawn.cpp index 186ec4ad5e..8014dfcabc 100644 --- a/src/bun.js/bindings/bun-spawn.cpp +++ b/src/bun.js/bindings/bun-spawn.cpp @@ -2,6 +2,7 @@ #if OS(LINUX) +#include #include #include #include @@ -111,10 +112,29 @@ extern "C" ssize_t posix_spawn_bun( break; } case FileActionType::Dup2: { - // Even if the file descrtiptors are the same, we still need to - // call dup2() because it will reset the close-on-exec flag. - if (dup2(action.fds[0], action.fds[1]) == -1) { - return childFailed(); + // Note: If oldfd is a valid file descriptor, and newfd has the same + // value as oldfd, then dup2() does nothing, and returns newfd. + if (action.fds[0] == action.fds[1]) { + int prevErrno = errno; + errno = 0; + + // Remove the O_CLOEXEC flag + // If we don't do this, then the process will have an already-closed file descriptor + int mask = fcntl(action.fds[0], F_GETFD, 0); + mask ^= FD_CLOEXEC; + fcntl(action.fds[0], F_SETFD, mask); + + if (errno != 0) { + return childFailed(); + } + + // Restore errno + errno = prevErrno; + } else { + // dup2 creates a new file descriptor without O_CLOEXEC set + if (dup2(action.fds[0], action.fds[1]) == -1) { + return childFailed(); + } } current_max_fd = std::max(current_max_fd, action.fds[1]); diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 282682de95..1125db6b18 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -1,6 +1,5 @@ // when we don't want to use @cInclude, we can just stick wrapper functions here #include "root.h" -#include #if !OS(WINDOWS) #include @@ -9,6 +8,10 @@ #include #include #include +#include +#include +#include +#include #else #include #include @@ -295,7 +298,7 @@ static inline void make_pos_h_l(unsigned long* pos_h, unsigned long* pos_l, extern "C" ssize_t sys_preadv2(int fd, const struct iovec* iov, int iovcnt, off_t offset, unsigned int flags) { - return syscall(SYS_preadv2, fd, iov, iovcnt, offset, offset>>32, RWF_NOWAIT); + return syscall(SYS_preadv2, fd, iov, iovcnt, offset, offset >> 32, RWF_NOWAIT); } extern "C" ssize_t sys_pwritev2(int fd, const struct iovec* iov, int iovcnt, off_t offset, unsigned int flags) @@ -320,3 +323,128 @@ extern "C" ssize_t pwritev2(int fd, const struct iovec* iov, int iovcnt, } #endif + +extern "C" void Bun__onExit(); +extern "C" int32_t bun_stdio_tty[3]; +#if !OS(WINDOWS) +static termios termios_to_restore_later[3]; +#endif + +extern "C" void bun_restore_stdio() +{ + +#if !OS(WINDOWS) + + // restore stdio + for (int32_t fd = 0; fd < 3; fd++) { + if (!bun_stdio_tty[fd]) + continue; + + sigset_t sa; + int err; + + // We might be a background job that doesn't own the TTY so block SIGTTOU + // before making the tcsetattr() call, otherwise that signal suspends us. + sigemptyset(&sa); + sigaddset(&sa, SIGTTOU); + + pthread_sigmask(SIG_BLOCK, &sa, nullptr); + do + err = tcsetattr(fd, TCSANOW, &termios_to_restore_later[fd]); + while (err == -1 && errno == EINTR); + pthread_sigmask(SIG_UNBLOCK, &sa, nullptr); + } +#endif +} + +#if !OS(WINDOWS) +extern "C" void onExitSignal(int sig) +{ + bun_restore_stdio(); + raise(sig); +} +#endif + +extern "C" void bun_initialize_process() +{ + // Disable printf() buffering. We buffer it ourselves. + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + +#if OS(LINUX) + // Prevent leaking inherited file descriptors on Linux + // This is less of an issue for macOS due to posix_spawn + // This is best effort, not all linux kernels support close_range or CLOSE_RANGE_CLOEXEC + bun_close_range(0, ~0U, CLOSE_RANGE_CLOEXEC); +#endif + +#if OS(LINUX) || OS(DARWIN) + + int devNullFd_ = -1; + bool anyTTYs = false; + + const auto setDevNullFd = [&](int target_fd) -> void { + if (devNullFd_ == -1) { + do { + devNullFd_ = open("/dev/null", O_RDWR | O_CLOEXEC, 0); + } while (devNullFd_ < 0 and errno == EINTR); + }; + + if (devNullFd_ == target_fd) { + devNullFd_ = -1; + return; + } + + ASSERT(devNullFd_ != -1); + int err; + do { + err = dup2(devNullFd_, target_fd); + } while (err < 0 && errno == EINTR); + + if (UNLIKELY(err != 0)) { + abort(); + } + }; + + for (int fd = 0; fd < 3; fd++) { + int result = isatty(fd); + if (result == 0) { + if (UNLIKELY(errno == EBADF)) { + // the fd is invalid, let's make sure it's always valid + setDevNullFd(fd); + } + } else { + bun_stdio_tty[fd] = 1; + int err = 0; + + do { + err = tcgetattr(fd, &termios_to_restore_later[fd]); + } while (err == -1 && errno == EINTR); + + if (LIKELY(err == 0)) { + anyTTYs = true; + } + } + } + + ASSERT(devNullFd_ == -1 || devNullFd_ > 2); + if (devNullFd_ > 2) { + close(devNullFd_); + } + + // Restore TTY state on exit + if (anyTTYs) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + + sa.sa_flags = SA_RESETHAND; + sa.sa_handler = onExitSignal; + + sigaction(SIGTERM, &sa, nullptr); + sigaction(SIGINT, &sa, nullptr); + } +#endif + + atexit(Bun__onExit); +} \ No newline at end of file diff --git a/src/bun.zig b/src/bun.zig index 3f1c051e37..e68db2a621 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -2809,8 +2809,6 @@ pub fn linuxKernelVersion() Semver.Version { return @import("./analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); } -pub const WindowsSpawnWorkaround = @import("./child_process_windows.zig"); - pub const exe_suffix = if (Environment.isWindows) ".exe" else ""; pub const spawnSync = @This().spawn.sync.spawn; diff --git a/src/c.zig b/src/c.zig index a01d6f4ab4..150dae475c 100644 --- a/src/c.zig +++ b/src/c.zig @@ -464,3 +464,6 @@ pub fn dlopen(filename: [:0]const u8, flags: i32) ?*anyopaque { } pub extern "C" fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int; + +pub extern "C" fn bun_initialize_process() void; +pub extern "C" fn bun_restore_stdio() void; diff --git a/src/child_process_windows.zig b/src/child_process_windows.zig deleted file mode 100644 index 452bd1ee37..0000000000 --- a/src/child_process_windows.zig +++ /dev/null @@ -1,743 +0,0 @@ -//! TODO: Delete this entire file once https://github.com/ziglang/zig/issues/18694 is resolved. -const bun = @import("root").bun; -const std = @import("std"); - -const os = std.os; -const windows = os.windows; -const mem = std.mem; -const unicode = std.unicode; -const fs = std.fs; -const math = std.math; - -const File = fs.File; - -const ChildProcess = std.ChildProcess; -const SpawnError = ChildProcess.SpawnError; -const StdIo = ChildProcess.StdIo; -const EnvMap = std.process.EnvMap; - -pub fn toUTF16Alloc(alloc: mem.Allocator, bytes: []const u8) ![:0]u16 { - return bun.strings.toUTF16AllocForReal(alloc, bytes, false, true); -} -const utf8ToUtf16Le = bun.strings.convertUTF8toUTF16InBuffer; - -pub fn spawnWindows(self: *ChildProcess) SpawnError!void { - const saAttr = windows.SECURITY_ATTRIBUTES{ - .nLength = @sizeOf(windows.SECURITY_ATTRIBUTES), - .bInheritHandle = windows.TRUE, - .lpSecurityDescriptor = null, - }; - - const any_ignore = (self.stdin_behavior == StdIo.Ignore or self.stdout_behavior == StdIo.Ignore or self.stderr_behavior == StdIo.Ignore); - - const nul_handle = if (any_ignore) - // "\Device\Null" or "\??\NUL" - windows.OpenFile(&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' }, .{ - .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, - .share_access = windows.FILE_SHARE_READ, - .creation = windows.OPEN_EXISTING, - .io_mode = .blocking, - }) catch |err| switch (err) { - error.PathAlreadyExists => unreachable, // not possible for "NUL" - error.PipeBusy => unreachable, // not possible for "NUL" - error.FileNotFound => unreachable, // not possible for "NUL" - error.AccessDenied => unreachable, // not possible for "NUL" - error.NameTooLong => unreachable, // not possible for "NUL" - error.WouldBlock => unreachable, // not possible for "NUL" - error.NetworkNotFound => unreachable, // not possible for "NUL" - else => |e| return e, - } - else - undefined; - defer { - if (any_ignore) os.close(nul_handle); - } - if (any_ignore) { - try windows.SetHandleInformation(nul_handle, windows.HANDLE_FLAG_INHERIT, 0); - } - - var g_hChildStd_IN_Rd: ?windows.HANDLE = null; - var g_hChildStd_IN_Wr: ?windows.HANDLE = null; - switch (self.stdin_behavior) { - StdIo.Pipe => { - try windowsMakePipeIn(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr); - }, - StdIo.Ignore => { - g_hChildStd_IN_Rd = nul_handle; - }, - StdIo.Inherit => { - g_hChildStd_IN_Rd = windows.GetStdHandle(windows.STD_INPUT_HANDLE) catch null; - }, - StdIo.Close => { - g_hChildStd_IN_Rd = null; - }, - } - errdefer if (self.stdin_behavior == StdIo.Pipe) { - windowsDestroyPipe(g_hChildStd_IN_Rd, g_hChildStd_IN_Wr); - }; - - var g_hChildStd_OUT_Rd: ?windows.HANDLE = null; - var g_hChildStd_OUT_Wr: ?windows.HANDLE = null; - switch (self.stdout_behavior) { - StdIo.Pipe => { - try windowsMakeAsyncPipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr); - }, - StdIo.Ignore => { - g_hChildStd_OUT_Wr = nul_handle; - }, - StdIo.Inherit => { - g_hChildStd_OUT_Wr = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE) catch null; - }, - StdIo.Close => { - g_hChildStd_OUT_Wr = null; - }, - } - errdefer if (self.stdin_behavior == StdIo.Pipe) { - windowsDestroyPipe(g_hChildStd_OUT_Rd, g_hChildStd_OUT_Wr); - }; - - var g_hChildStd_ERR_Rd: ?windows.HANDLE = null; - var g_hChildStd_ERR_Wr: ?windows.HANDLE = null; - switch (self.stderr_behavior) { - StdIo.Pipe => { - try windowsMakeAsyncPipe(&g_hChildStd_ERR_Rd, &g_hChildStd_ERR_Wr, &saAttr); - }, - StdIo.Ignore => { - g_hChildStd_ERR_Wr = nul_handle; - }, - StdIo.Inherit => { - g_hChildStd_ERR_Wr = windows.GetStdHandle(windows.STD_ERROR_HANDLE) catch null; - }, - StdIo.Close => { - g_hChildStd_ERR_Wr = null; - }, - } - errdefer if (self.stdin_behavior == StdIo.Pipe) { - windowsDestroyPipe(g_hChildStd_ERR_Rd, g_hChildStd_ERR_Wr); - }; - - const cmd_line = try windowsCreateCommandLine(self.allocator, self.argv); - defer self.allocator.free(cmd_line); - - var siStartInfo = windows.STARTUPINFOW{ - .cb = @sizeOf(windows.STARTUPINFOW), - .hStdError = g_hChildStd_ERR_Wr, - .hStdOutput = g_hChildStd_OUT_Wr, - .hStdInput = g_hChildStd_IN_Rd, - .dwFlags = windows.STARTF_USESTDHANDLES, - - .lpReserved = null, - .lpDesktop = null, - .lpTitle = null, - .dwX = 0, - .dwY = 0, - .dwXSize = 0, - .dwYSize = 0, - .dwXCountChars = 0, - .dwYCountChars = 0, - .dwFillAttribute = 0, - .wShowWindow = 0, - .cbReserved2 = 0, - .lpReserved2 = null, - }; - var piProcInfo: windows.PROCESS_INFORMATION = undefined; - - const cwd_w = if (self.cwd) |cwd| try toUTF16Alloc(self.allocator, cwd) else null; - defer if (cwd_w) |cwd| self.allocator.free(cwd); - const cwd_w_ptr = if (cwd_w) |cwd| cwd.ptr else null; - - const maybe_envp_buf = if (self.env_map) |env_map| try createWindowsEnvBlock(self.allocator, env_map) else null; - defer if (maybe_envp_buf) |envp_buf| self.allocator.free(envp_buf); - const envp_ptr = if (maybe_envp_buf) |envp_buf| envp_buf.ptr else null; - - const app_name_utf8 = self.argv[0]; - const app_name_is_absolute = fs.path.isAbsolute(app_name_utf8); - - // the cwd set in ChildProcess is in effect when choosing the executable path - // to match posix semantics - var cwd_path_w_needs_free = false; - const cwd_path_w = x: { - // If the app name is absolute, then we need to use its dirname as the cwd - if (app_name_is_absolute) { - cwd_path_w_needs_free = true; - const dir = fs.path.dirname(app_name_utf8).?; - break :x try toUTF16Alloc(self.allocator, dir); - } else if (self.cwd) |cwd| { - cwd_path_w_needs_free = true; - break :x try toUTF16Alloc(self.allocator, cwd); - } else { - break :x &[_:0]u16{}; // empty for cwd - } - }; - defer if (cwd_path_w_needs_free) self.allocator.free(cwd_path_w); - - // If the app name has more than just a filename, then we need to separate that - // into the basename and dirname and use the dirname as an addition to the cwd - // path. This is because NtQueryDirectoryFile cannot accept FileName params with - // path separators. - const app_basename_utf8 = fs.path.basename(app_name_utf8); - // If the app name is absolute, then the cwd will already have the app's dirname in it, - // so only populate app_dirname if app name is a relative path with > 0 path separators. - const maybe_app_dirname_utf8 = if (!app_name_is_absolute) fs.path.dirname(app_name_utf8) else null; - const app_dirname_w: ?[:0]u16 = x: { - if (maybe_app_dirname_utf8) |app_dirname_utf8| { - break :x try toUTF16Alloc(self.allocator, app_dirname_utf8); - } - break :x null; - }; - defer if (app_dirname_w != null) self.allocator.free(app_dirname_w.?); - - const app_name_w = try toUTF16Alloc(self.allocator, app_basename_utf8); - defer self.allocator.free(app_name_w); - - const cmd_line_w = try toUTF16Alloc(self.allocator, cmd_line); - defer self.allocator.free(cmd_line_w); - - run: { - const PATH: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATH")) orelse &[_:0]u16{}; - const PATHEXT: [:0]const u16 = std.os.getenvW(unicode.utf8ToUtf16LeStringLiteral("PATHEXT")) orelse &[_:0]u16{}; - - var app_buf = std.ArrayListUnmanaged(u16){}; - defer app_buf.deinit(self.allocator); - - try app_buf.appendSlice(self.allocator, app_name_w); - - var dir_buf = std.ArrayListUnmanaged(u16){}; - defer dir_buf.deinit(self.allocator); - - if (cwd_path_w.len > 0) { - try dir_buf.appendSlice(self.allocator, cwd_path_w); - } - if (app_dirname_w) |app_dir| { - if (dir_buf.items.len > 0) try dir_buf.append(self.allocator, fs.path.sep); - try dir_buf.appendSlice(self.allocator, app_dir); - } - if (dir_buf.items.len > 0) { - // Need to normalize the path, openDirW can't handle things like double backslashes - const normalized_len = windows.normalizePath(u16, dir_buf.items) catch return error.BadPathName; - dir_buf.shrinkRetainingCapacity(normalized_len); - } - - windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo) catch |no_path_err| { - const original_err = switch (no_path_err) { - error.FileNotFound, error.InvalidExe, error.AccessDenied => |e| e, - error.UnrecoverableInvalidExe => return error.InvalidExe, - else => |e| return e, - }; - - // If the app name had path separators, that disallows PATH searching, - // and there's no need to search the PATH if the app name is absolute. - // We still search the path if the cwd is absolute because of the - // "cwd set in ChildProcess is in effect when choosing the executable path - // to match posix semantics" behavior--we don't want to skip searching - // the PATH just because we were trying to set the cwd of the child process. - if (app_dirname_w != null or app_name_is_absolute) { - return original_err; - } - - var it = mem.tokenizeScalar(u16, PATH, ';'); - while (it.next()) |search_path| { - dir_buf.clearRetainingCapacity(); - try dir_buf.appendSlice(self.allocator, search_path); - // Need to normalize the path, some PATH values can contain things like double - // backslashes which openDirW can't handle - const normalized_len = windows.normalizePath(u16, dir_buf.items) catch continue; - dir_buf.shrinkRetainingCapacity(normalized_len); - - if (windowsCreateProcessPathExt(self.allocator, &dir_buf, &app_buf, PATHEXT, cmd_line_w.ptr, envp_ptr, cwd_w_ptr, &siStartInfo, &piProcInfo)) { - break :run; - } else |err| switch (err) { - error.FileNotFound, error.AccessDenied, error.InvalidExe => continue, - error.UnrecoverableInvalidExe => return error.InvalidExe, - else => |e| return e, - } - } else { - return original_err; - } - }; - } - - if (g_hChildStd_IN_Wr) |h| { - self.stdin = File{ .handle = h }; - } else { - self.stdin = null; - } - if (g_hChildStd_OUT_Rd) |h| { - self.stdout = File{ .handle = h }; - } else { - self.stdout = null; - } - if (g_hChildStd_ERR_Rd) |h| { - self.stderr = File{ .handle = h }; - } else { - self.stderr = null; - } - - self.id = piProcInfo.hProcess; - self.thread_handle = piProcInfo.hThread; - self.term = null; - - if (self.stdin_behavior == StdIo.Pipe) { - os.close(g_hChildStd_IN_Rd.?); - } - if (self.stderr_behavior == StdIo.Pipe) { - os.close(g_hChildStd_ERR_Wr.?); - } - if (self.stdout_behavior == StdIo.Pipe) { - os.close(g_hChildStd_OUT_Wr.?); - } -} - -/// Caller must dealloc. -fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { - var buf = std.ArrayList(u8).init(allocator); - defer buf.deinit(); - - for (argv, 0..) |arg, arg_i| { - if (arg_i != 0) try buf.append(' '); - if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { - try buf.appendSlice(arg); - continue; - } - try buf.append('"'); - var backslash_count: usize = 0; - for (arg) |byte| { - switch (byte) { - '\\' => backslash_count += 1, - '"' => { - try buf.appendNTimes('\\', backslash_count * 2 + 1); - try buf.append('"'); - backslash_count = 0; - }, - else => { - try buf.appendNTimes('\\', backslash_count); - try buf.append(byte); - backslash_count = 0; - }, - } - } - try buf.appendNTimes('\\', backslash_count * 2); - try buf.append('"'); - } - - return buf.toOwnedSliceSentinel(0); -} - -fn windowsDestroyPipe(rd: ?windows.HANDLE, wr: ?windows.HANDLE) void { - if (rd) |h| os.close(h); - if (wr) |h| os.close(h); -} - -fn windowsMakePipeIn(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { - var rd_h: windows.HANDLE = undefined; - var wr_h: windows.HANDLE = undefined; - try windows.CreatePipe(&rd_h, &wr_h, sattr); - errdefer windowsDestroyPipe(rd_h, wr_h); - try windows.SetHandleInformation(wr_h, windows.HANDLE_FLAG_INHERIT, 0); - rd.* = rd_h; - wr.* = wr_h; -} - -var pipe_name_counter = std.atomic.Value(u32).init(1); - -fn windowsMakeAsyncPipe(rd: *?windows.HANDLE, wr: *?windows.HANDLE, sattr: *const windows.SECURITY_ATTRIBUTES) !void { - var tmp_bufw: [128]u16 = undefined; - - // Anonymous pipes are built upon Named pipes. - // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe - // Asynchronous (overlapped) read and write operations are not supported by anonymous pipes. - // https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipe-operations - const pipe_path = blk: { - var tmp_buf: [128]u8 = undefined; - // Forge a random path for the pipe. - const pipe_path = std.fmt.bufPrintZ( - &tmp_buf, - "\\\\.\\pipe\\zig-childprocess-{d}-{d}", - .{ windows.kernel32.GetCurrentProcessId(), pipe_name_counter.fetchAdd(1, .Monotonic) }, - ) catch unreachable; - const buf_2 = utf8ToUtf16Le(&tmp_bufw, pipe_path); - tmp_bufw[buf_2.len] = 0; - break :blk tmp_bufw[0..buf_2.len :0]; - }; - - // Create the read handle that can be used with overlapped IO ops. - const read_handle = windows.kernel32.CreateNamedPipeW( - pipe_path.ptr, - windows.PIPE_ACCESS_INBOUND | windows.FILE_FLAG_OVERLAPPED, - windows.PIPE_TYPE_BYTE, - 1, - 4096, - 4096, - 0, - sattr, - ); - if (read_handle == windows.INVALID_HANDLE_VALUE) { - switch (windows.kernel32.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - } - errdefer os.close(read_handle); - - var sattr_copy = sattr.*; - const write_handle = windows.kernel32.CreateFileW( - pipe_path.ptr, - windows.GENERIC_WRITE, - 0, - &sattr_copy, - windows.OPEN_EXISTING, - windows.FILE_ATTRIBUTE_NORMAL, - null, - ); - if (write_handle == windows.INVALID_HANDLE_VALUE) { - switch (windows.kernel32.GetLastError()) { - else => |err| return windows.unexpectedError(err), - } - } - errdefer os.close(write_handle); - - try windows.SetHandleInformation(read_handle, windows.HANDLE_FLAG_INHERIT, 0); - - rd.* = read_handle; - wr.* = write_handle; -} - -pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u16 { - // count bytes needed - const max_chars_needed = x: { - var max_chars_needed: usize = 4; // 4 for the final 4 null bytes - var it = env_map.iterator(); - while (it.next()) |pair| { - // +1 for '=' - // +1 for null byte - max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2; - } - break :x max_chars_needed; - }; - const result = try allocator.alloc(u16, max_chars_needed); - errdefer allocator.free(result); - - var it = env_map.iterator(); - var i: usize = 0; - while (it.next()) |pair| { - i += utf8ToUtf16Le(result[i..], pair.key_ptr.*).len; - result[i] = '='; - i += 1; - i += utf8ToUtf16Le(result[i..], pair.value_ptr.*).len; - result[i] = 0; - i += 1; - } - result[i] = 0; - i += 1; - result[i] = 0; - i += 1; - result[i] = 0; - i += 1; - result[i] = 0; - i += 1; - return try allocator.realloc(result, i); -} - -/// Expects `app_buf` to contain exactly the app name, and `dir_buf` to contain exactly the dir path. -/// After return, `app_buf` will always contain exactly the app name and `dir_buf` will always contain exactly the dir path. -/// Note: `app_buf` should not contain any leading path separators. -/// Note: If the dir is the cwd, dir_buf should be empty (len = 0). -fn windowsCreateProcessPathExt( - allocator: mem.Allocator, - dir_buf: *std.ArrayListUnmanaged(u16), - app_buf: *std.ArrayListUnmanaged(u16), - pathext: [:0]const u16, - cmd_line: [*:0]u16, - envp_ptr: ?[*]u16, - cwd_ptr: ?[*:0]u16, - lpStartupInfo: *windows.STARTUPINFOW, - lpProcessInformation: *windows.PROCESS_INFORMATION, -) !void { - const app_name_len = app_buf.items.len; - const dir_path_len = dir_buf.items.len; - - if (app_name_len == 0) return error.FileNotFound; - - defer app_buf.shrinkRetainingCapacity(app_name_len); - defer dir_buf.shrinkRetainingCapacity(dir_path_len); - - // The name of the game here is to avoid CreateProcessW calls at all costs, - // and only ever try calling it when we have a real candidate for execution. - // Secondarily, we want to minimize the number of syscalls used when checking - // for each PATHEXT-appended version of the app name. - // - // An overview of the technique used: - // - Open the search directory for iteration (either cwd or a path from PATH) - // - Use NtQueryDirectoryFile with a wildcard filename of `*` to - // check if anything that could possibly match either the unappended version - // of the app name or any of the versions with a PATHEXT value appended exists. - // - If the wildcard NtQueryDirectoryFile call found nothing, we can exit early - // without needing to use PATHEXT at all. - // - // This allows us to use a sequence - // for any directory that doesn't contain any possible matches, instead of having - // to use a separate look up for each individual filename combination (unappended + - // each PATHEXT appended). For directories where the wildcard *does* match something, - // we iterate the matches and take note of any that are either the unappended version, - // or a version with a supported PATHEXT appended. We then try calling CreateProcessW - // with the found versions in the appropriate order. - - var dir = dir: { - // needs to be null-terminated - try dir_buf.append(allocator, 0); - defer dir_buf.shrinkRetainingCapacity(dir_path_len); - const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch - return error.FileNotFound; - }; - defer dir.close(); - - // Add wildcard and null-terminator - try app_buf.append(allocator, '*'); - try app_buf.append(allocator, 0); - const app_name_wildcard = app_buf.items[0 .. app_buf.items.len - 1 :0]; - - // This 2048 is arbitrary, we just want it to be large enough to get multiple FILE_DIRECTORY_INFORMATION entries - // returned per NtQueryDirectoryFile call. - var file_information_buf: [2048]u8 align(@alignOf(os.windows.FILE_DIRECTORY_INFORMATION)) = undefined; - const file_info_maximum_single_entry_size = @sizeOf(windows.FILE_DIRECTORY_INFORMATION) + (windows.NAME_MAX * 2); - if (file_information_buf.len < file_info_maximum_single_entry_size) { - @compileError("file_information_buf must be large enough to contain at least one maximum size FILE_DIRECTORY_INFORMATION entry"); - } - var io_status: windows.IO_STATUS_BLOCK = undefined; - - const num_supported_pathext = @typeInfo(CreateProcessSupportedExtension).Enum.fields.len; - var pathext_seen = [_]bool{false} ** num_supported_pathext; - var any_pathext_seen = false; - var unappended_exists = false; - - // Fully iterate the wildcard matches via NtQueryDirectoryFile and take note of all versions - // of the app_name we should try to spawn. - // Note: This is necessary because the order of the files returned is filesystem-dependent: - // On NTFS, `blah.exe*` will always return `blah.exe` first if it exists. - // On FAT32, it's possible for something like `blah.exe.obj` to be returned first. - while (true) { - const app_name_len_bytes = math.cast(u16, app_name_wildcard.len * 2) orelse return error.NameTooLong; - var app_name_unicode_string = windows.UNICODE_STRING{ - .Length = app_name_len_bytes, - .MaximumLength = app_name_len_bytes, - .Buffer = @constCast(app_name_wildcard.ptr), - }; - const rc = windows.ntdll.NtQueryDirectoryFile( - dir.fd, - null, - null, - null, - &io_status, - &file_information_buf, - file_information_buf.len, - .FileDirectoryInformation, - windows.FALSE, // single result - &app_name_unicode_string, - windows.FALSE, // restart iteration - ); - - // If we get nothing with the wildcard, then we can just bail out - // as we know appending PATHEXT will not yield anything. - switch (rc) { - .SUCCESS => {}, - .NO_SUCH_FILE => return error.FileNotFound, - .NO_MORE_FILES => break, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - - // According to the docs, this can only happen if there is not enough room in the - // buffer to write at least one complete FILE_DIRECTORY_INFORMATION entry. - // Therefore, this condition should not be possible to hit with the buffer size we use. - std.debug.assert(io_status.Information != 0); - - var it = windows.FileInformationIterator(windows.FILE_DIRECTORY_INFORMATION){ .buf = &file_information_buf }; - while (it.next()) |info| { - // Skip directories - if (info.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) continue; - const filename = @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2]; - // Because all results start with the app_name since we're using the wildcard `app_name*`, - // if the length is equal to app_name then this is an exact match - if (filename.len == app_name_len) { - // Note: We can't break early here because it's possible that the unappended version - // fails to spawn, in which case we still want to try the PATHEXT appended versions. - unappended_exists = true; - } else if (windowsCreateProcessSupportsExtension(filename[app_name_len..])) |pathext_ext| { - pathext_seen[@intFromEnum(pathext_ext)] = true; - any_pathext_seen = true; - } - } - } - - const unappended_err = unappended: { - if (unappended_exists) { - if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { - '/', '\\' => {}, - else => try dir_buf.append(allocator, fs.path.sep), - }; - try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); - try dir_buf.append(allocator, 0); - const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - - if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { - return; - } else |err| switch (err) { - error.FileNotFound, - error.AccessDenied, - => break :unappended err, - error.InvalidExe => { - // On InvalidExe, if the extension of the app name is .exe then - // it's treated as an unrecoverable error. Otherwise, it'll be - // skipped as normal. - const app_name = app_buf.items[0..app_name_len]; - const ext_start = std.mem.lastIndexOfScalar(u16, app_name, '.') orelse break :unappended err; - const ext = app_name[ext_start..]; - if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { - return error.UnrecoverableInvalidExe; - } - break :unappended err; - }, - else => return err, - } - } - break :unappended error.FileNotFound; - }; - - if (!any_pathext_seen) return unappended_err; - - // Now try any PATHEXT appended versions that we've seen - var ext_it = mem.tokenizeScalar(u16, pathext, ';'); - while (ext_it.next()) |ext| { - const ext_enum = windowsCreateProcessSupportsExtension(ext) orelse continue; - if (!pathext_seen[@intFromEnum(ext_enum)]) continue; - - dir_buf.shrinkRetainingCapacity(dir_path_len); - if (dir_path_len != 0) switch (dir_buf.items[dir_buf.items.len - 1]) { - '/', '\\' => {}, - else => try dir_buf.append(allocator, fs.path.sep), - }; - try dir_buf.appendSlice(allocator, app_buf.items[0..app_name_len]); - try dir_buf.appendSlice(allocator, ext); - try dir_buf.append(allocator, 0); - const full_app_name = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; - - if (windowsCreateProcess(full_app_name.ptr, cmd_line, envp_ptr, cwd_ptr, lpStartupInfo, lpProcessInformation)) |_| { - return; - } else |err| switch (err) { - error.FileNotFound => continue, - error.AccessDenied => continue, - error.InvalidExe => { - // On InvalidExe, if the extension of the app name is .exe then - // it's treated as an unrecoverable error. Otherwise, it'll be - // skipped as normal. - if (windows.eqlIgnoreCaseWTF16(ext, unicode.utf8ToUtf16LeStringLiteral(".EXE"))) { - return error.UnrecoverableInvalidExe; - } - continue; - }, - else => return err, - } - } - - return unappended_err; -} - -// Should be kept in sync with `windowsCreateProcessSupportsExtension` -const CreateProcessSupportedExtension = enum { - bat, - cmd, - com, - exe, -}; - -/// Case-insensitive UTF-16 lookup -fn windowsCreateProcessSupportsExtension(ext: []const u16) ?CreateProcessSupportedExtension { - if (ext.len != 4) return null; - const State = enum { - start, - dot, - b, - ba, - c, - cm, - co, - e, - ex, - }; - var state: State = .start; - for (ext) |c| switch (state) { - .start => switch (c) { - '.' => state = .dot, - else => return null, - }, - .dot => switch (c) { - 'b', 'B' => state = .b, - 'c', 'C' => state = .c, - 'e', 'E' => state = .e, - else => return null, - }, - .b => switch (c) { - 'a', 'A' => state = .ba, - else => return null, - }, - .c => switch (c) { - 'm', 'M' => state = .cm, - 'o', 'O' => state = .co, - else => return null, - }, - .e => switch (c) { - 'x', 'X' => state = .ex, - else => return null, - }, - .ba => switch (c) { - 't', 'T' => return .bat, - else => return null, - }, - .cm => switch (c) { - 'd', 'D' => return .cmd, - else => return null, - }, - .co => switch (c) { - 'm', 'M' => return .com, - else => return null, - }, - .ex => switch (c) { - 'e', 'E' => return .exe, - else => return null, - }, - }; - return null; -} - -fn windowsCreateProcess(app_name: [*:0]u16, cmd_line: [*:0]u16, envp_ptr: ?[*]u16, cwd_ptr: ?[*:0]u16, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION) !void { - // TODO the docs for environment pointer say: - // > A pointer to the environment block for the new process. If this parameter - // > is NULL, the new process uses the environment of the calling process. - // > ... - // > An environment block can contain either Unicode or ANSI characters. If - // > the environment block pointed to by lpEnvironment contains Unicode - // > characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. - // > If this parameter is NULL and the environment block of the parent process - // > contains Unicode characters, you must also ensure that dwCreationFlags - // > includes CREATE_UNICODE_ENVIRONMENT. - // This seems to imply that we have to somehow know whether our process parent passed - // CREATE_UNICODE_ENVIRONMENT if we want to pass NULL for the environment parameter. - // Since we do not know this information that would imply that we must not pass NULL - // for the parameter. - // However this would imply that programs compiled with -DUNICODE could not pass - // environment variables to programs that were not, which seems unlikely. - // More investigation is needed. - return windows.CreateProcessW( - app_name, - cmd_line, - null, - null, - windows.TRUE, - windows.CREATE_UNICODE_ENVIRONMENT, - @as(?*anyopaque, @ptrCast(envp_ptr)), - cwd_ptr, - lpStartupInfo, - lpProcessInformation, - ); -} diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 7bb4f0fe31..3cbf9841f4 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -109,8 +109,6 @@ fn execTask(allocator: std.mem.Allocator, task_: string, cwd: string, _: string, const npm_args = 2 * @as(usize, @intCast(@intFromBool(npm_client != null))); const total = count + npm_args; var argv = allocator.alloc(string, total) catch return; - var proc: std.ChildProcess = undefined; - defer if (argv.len > 32) allocator.free(argv); if (npm_client) |client| { argv[0] = client.bin; @@ -146,19 +144,19 @@ fn execTask(allocator: std.mem.Allocator, task_: string, cwd: string, _: string, Output.disableBuffering(); defer Output.enableBuffering(); - proc = std.ChildProcess.init(argv, allocator); - proc.stdin_behavior = .Inherit; - proc.stdout_behavior = .Inherit; - proc.stderr_behavior = .Inherit; - proc.cwd = cwd; + _ = bun.spawnSync(&.{ + .argv = argv, + .envp = null, - if (Environment.isWindows) { - bun.WindowsSpawnWorkaround.spawnWindows(&proc) catch return; - } else { - proc.spawn() catch return; - } + .cwd = cwd, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, - _ = proc.wait() catch {}; + .windows = if (Environment.isWindows) .{ + .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), + } else {}, + }) catch return; } // We don't want to allocate memory each time @@ -1427,10 +1425,6 @@ pub const CreateCommand = struct { Output.pretty("\n", .{}); Output.flush(); - - var process = std.ChildProcess.init(install_args, ctx.allocator); - process.cwd = destination; - defer { Output.printErrorln("\n", .{}); Output.printStartEnd(start_time, std.time.nanoTimestamp()); @@ -1441,13 +1435,19 @@ pub const CreateCommand = struct { Output.flush(); } - if (Environment.isWindows) { - try bun.WindowsSpawnWorkaround.spawnWindows(&process); - } else { - try process.spawn(); - } - _ = try process.wait(); - _ = try process.kill(); + const process = try bun.spawnSync(&.{ + .argv = install_args, + .envp = null, + .cwd = destination, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, + + .windows = if (Environment.isWindows) .{ + .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(null)), + } else {}, + }); + _ = try process.unwrap(); } if (postinstall_tasks.items.len > 0) { diff --git a/src/cli/discord_command.zig b/src/cli/discord_command.zig index 46b3b37045..06fff1238f 100644 --- a/src/cli/discord_command.zig +++ b/src/cli/discord_command.zig @@ -12,7 +12,7 @@ const std = @import("std"); const open = @import("../open.zig"); pub const DiscordCommand = struct { - const discord_url: string = "https://bun.sh/discord"; + const discord_url = "https://bun.sh/discord"; pub fn exec(_: std.mem.Allocator) !void { open.openURL(discord_url); } diff --git a/src/install/dependency.zig b/src/install/dependency.zig index 06772b6b12..98f84fee82 100644 --- a/src/install/dependency.zig +++ b/src/install/dependency.zig @@ -968,12 +968,13 @@ pub fn parseWithTag( if (strings.indexOfChar(dependency, ':')) |protocol| { if (strings.eqlComptime(dependency[0..protocol], "file")) { const folder = brk: { - if (dependency[protocol + 1] == '/') { - if (dependency.len >= protocol + 2 and dependency[protocol + 2] == '/') { + if (dependency.len > protocol + 1 and dependency[protocol + 1] == '/') { + if (dependency.len > protocol + 2 and dependency[protocol + 2] == '/') { break :brk dependency[protocol + 3 ..]; } break :brk dependency[protocol + 2 ..]; } + break :brk dependency[protocol + 1 ..]; }; diff --git a/src/install/install.zig b/src/install/install.zig index 64ab127e95..5e5b6092cd 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -301,7 +301,7 @@ const NetworkTask = struct { name: string, allocator: std.mem.Allocator, scope: *const Npm.Registry.Scope, - loaded_manifest: ?Npm.PackageManifest, + loaded_manifest: ?*const Npm.PackageManifest, warn_on_error: bool, ) !void { this.url_buf = blk: { @@ -337,6 +337,21 @@ const NetworkTask = struct { return error.InvalidURL; } + + if (!(tmp.hasPrefixComptime("https://") or tmp.hasPrefixComptime("http://"))) { + const msg = .{ + .fmt = "Registry URL must be http:// or https://\nReceived: \"{}\"", + .args = .{tmp}, + }; + + if (warn_on_error) + this.package_manager.log.addWarningFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable + else + this.package_manager.log.addErrorFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable; + + return error.InvalidURL; + } + // This actually duplicates the string! So we defer deref the WTF managed one above. break :blk try tmp.toOwnedSlice(allocator); }; @@ -408,7 +423,7 @@ const NetworkTask = struct { this.callback = .{ .package_manifest = .{ .name = try strings.StringOrTinyString.initAppendIfNeeded(name, *FileSystem.FilenameStore, &FileSystem.FilenameStore.instance), - .loaded_manifest = loaded_manifest, + .loaded_manifest = if (loaded_manifest) |manifest| manifest.* else null, }, }; @@ -450,6 +465,16 @@ const NetworkTask = struct { this.url_buf = tarball_url; } + if (!(strings.hasPrefixComptime(this.url_buf, "https://") or strings.hasPrefixComptime(this.url_buf, "http://"))) { + const msg = .{ + .fmt = "Expected tarball URL to start with https:// or http://, got {} while fetching package {}", + .args = .{ bun.fmt.QuotedFormatter{ .text = this.url_buf }, bun.fmt.QuotedFormatter{ .text = tarball.name.slice() } }, + }; + + this.package_manager.log.addErrorFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable; + return error.InvalidURL; + } + this.response_buffer = try MutableString.init(allocator, 0); this.allocator = allocator; @@ -3876,7 +3901,7 @@ pub const PackageManager = struct { name_str, this.allocator, this.scopeForPackageName(name_str), - loaded_manifest, + if (loaded_manifest) |*manifest| manifest else null, dependency.behavior.isOptional() or !this.options.do.install_peer_dependencies, ); this.enqueueNetworkTask(network_task); diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index c72034cdec..6295d852eb 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -408,7 +408,7 @@ pub const LifecycleScriptSubprocess = struct { }); if (comptime log_level.isVerbose()) { - Output.prettyErrorln("[LifecycleScriptSubprocess] Starting scripts for \"{s}\"", .{ + Output.prettyErrorln("[Scripts] Starting scripts for \"{s}\"", .{ list.package_name, }); } diff --git a/src/main.zig b/src/main.zig index ba06ced75e..697459c7f5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,14 +21,9 @@ pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, addr const CrashReporter = @import("./crash_reporter.zig"); extern fn bun_warn_avx_missing(url: [*:0]const u8) void; - pub extern "C" var _environ: ?*anyopaque; pub extern "C" var environ: ?*anyopaque; -// TODO: when https://github.com/ziglang/zig/pull/18692 merges, use std.os.windows for this -extern fn SetConsoleMode(console_handle: *anyopaque, mode: u32) u32; -extern fn SetStdHandle(nStdHandle: u32, hHandle: *anyopaque) u32; -pub extern "kernel32" fn SetConsoleCP(wCodePageID: std.os.windows.UINT) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; pub fn main() void { // This should appear before we make any calls at all to libuv. // So it's safest to put it very early in the main function. @@ -39,6 +34,8 @@ pub fn main() void { @ptrCast(&bun.Mimalloc.mi_calloc), @ptrCast(&bun.Mimalloc.mi_free), ); + environ = @ptrCast(std.os.environ.ptr); + _environ = @ptrCast(std.os.environ.ptr); } bun.initArgv(bun.default_allocator) catch |err| { @@ -48,72 +45,8 @@ pub fn main() void { if (Environment.isRelease and Environment.isPosix) CrashReporter.start() catch unreachable; - if (Environment.isWindows) { - environ = @ptrCast(std.os.environ.ptr); - _environ = @ptrCast(std.os.environ.ptr); - const peb = std.os.windows.peb(); - var stdout = peb.ProcessParameters.hStdOutput; - var stderr = peb.ProcessParameters.hStdError; - var stdin = peb.ProcessParameters.hStdInput; - - const handle_identifiers = &.{ std.os.windows.STD_INPUT_HANDLE, std.os.windows.STD_OUTPUT_HANDLE, std.os.windows.STD_ERROR_HANDLE }; - const handles = &.{ &stdin, &stdout, &stderr }; - inline for (0..3) |fd_i| { - if (handles[fd_i].* == std.os.windows.INVALID_HANDLE_VALUE) { - handles[fd_i].* = bun.windows.CreateFileW( - comptime bun.strings.w("NUL" ++ .{0}).ptr, - if (fd_i > 0) std.os.windows.GENERIC_WRITE else std.os.windows.GENERIC_READ, - 0, - null, - std.os.windows.OPEN_EXISTING, - 0, - null, - ); - _ = SetStdHandle(handle_identifiers[fd_i], handles[fd_i].*); - } - } - - bun.win32.STDERR_FD = if (stderr != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stderr) else bun.invalid_fd; - bun.win32.STDOUT_FD = if (stdout != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdout) else bun.invalid_fd; - bun.win32.STDIN_FD = if (stdin != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdin) else bun.invalid_fd; - - bun.Output.buffered_stdin.unbuffered_reader.context.handle = bun.win32.STDIN_FD; - - const w = std.os.windows; - - // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp - const CP_UTF8 = 65001; - _ = w.kernel32.SetConsoleOutputCP(CP_UTF8); - _ = SetConsoleCP(CP_UTF8); - const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200; - const ENABLE_PROCESSED_OUTPUT = 0x0001; - - var mode: w.DWORD = undefined; - if (w.kernel32.GetConsoleMode(stdout, &mode) != 0) { - _ = SetConsoleMode(stdout, mode | ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); - } - - if (w.kernel32.GetConsoleMode(stderr, &mode) != 0) { - _ = SetConsoleMode(stderr, mode | ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); - } - - if (w.kernel32.GetConsoleMode(stdin, &mode) != 0) { - _ = SetConsoleMode(stdin, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); - } - } - bun.start_time = std.time.nanoTimestamp(); - - const stdout = bun.sys.File.from(std.io.getStdOut()); - const stderr = bun.sys.File.from(std.io.getStdErr()); - var output_source = Output.Source.init(stdout, stderr); - - Output.Source.set(&output_source); - - if (comptime Environment.isDebug) { - bun.Output.initScopedDebugWriterAtStartup(); - } - + Output.Source.Stdio.init(); defer Output.flush(); if (Environment.isX64 and Environment.enableSIMD and Environment.isPosix) { bun_warn_avx_missing(@import("./cli/upgrade_command.zig").Version.Bun__githubBaselineURL.ptr); diff --git a/src/open.zig b/src/open.zig index ad7db7128f..ea2bd6d64c 100644 --- a/src/open.zig +++ b/src/open.zig @@ -22,16 +22,32 @@ fn fallback(url: string) void { Output.flush(); } -pub fn openURL(url: string) void { +pub fn openURL(url: stringZ) void { if (comptime Environment.isWasi) return fallback(url); - var args_buf = [_]string{ opener, url }; - var child_process = std.ChildProcess.init(&args_buf, default_allocator); - child_process.stderr_behavior = .Pipe; - child_process.stdin_behavior = .Ignore; - child_process.stdout_behavior = .Pipe; - child_process.spawn() catch return fallback(url); - _ = child_process.wait() catch 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)), + } else {}, + }) catch break :maybe_fallback) { + // don't fallback: + .result => |*result| if (result.isOK()) return, + .err => {}, + } + } + + fallback(url); } pub const Editor = enum(u8) { diff --git a/src/output.zig b/src/output.zig index be5fd5956a..41b01dbb58 100644 --- a/src/output.zig +++ b/src/output.zig @@ -133,6 +133,132 @@ pub const Source = struct { return false; } + export var bun_stdio_tty: [3]i32 = .{ 0, 0, 0 }; + + const WindowsStdio = struct { + const w = std.os.windows; + + // TODO: when https://github.com/ziglang/zig/pull/18692 merges, use std.os.windows for this + extern fn SetConsoleMode(console_handle: *anyopaque, mode: u32) u32; + extern fn SetStdHandle(nStdHandle: u32, hHandle: *anyopaque) u32; + extern fn GetConsoleOutputCP() u32; + pub extern "kernel32" fn SetConsoleCP(wCodePageID: std.os.windows.UINT) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; + + pub var console_mode = [3]?u32{ null, null, null }; + pub var console_codepage = @as(u32, 0); + pub var console_output_codepage = @as(u32, 0); + + pub fn restore() void { + const peb = std.os.windows.peb(); + const stdout = peb.ProcessParameters.hStdOutput; + const stderr = peb.ProcessParameters.hStdError; + const stdin = peb.ProcessParameters.hStdInput; + + const handles = &.{ &stdin, &stdout, &stderr }; + inline for (console_mode, handles) |mode, handle| { + if (mode) |m| { + _ = SetConsoleMode(handle.*, m); + } + } + + if (console_output_codepage != 0) + _ = w.kernel32.SetConsoleOutputCP(console_output_codepage); + + if (console_codepage != 0) + _ = SetConsoleCP(console_codepage); + } + + pub fn init() void { + bun.windows.libuv.uv_disable_stdio_inheritance(); + + const peb = std.os.windows.peb(); + var stdout = peb.ProcessParameters.hStdOutput; + var stderr = peb.ProcessParameters.hStdError; + var stdin = peb.ProcessParameters.hStdInput; + + const handle_identifiers = &.{ std.os.windows.STD_INPUT_HANDLE, std.os.windows.STD_OUTPUT_HANDLE, std.os.windows.STD_ERROR_HANDLE }; + const handles = &.{ &stdin, &stdout, &stderr }; + inline for (0..3) |fd_i| { + if (handles[fd_i].* == std.os.windows.INVALID_HANDLE_VALUE) { + handles[fd_i].* = bun.windows.CreateFileW( + comptime bun.strings.w("NUL" ++ .{0}).ptr, + if (fd_i > 0) std.os.windows.GENERIC_WRITE else std.os.windows.GENERIC_READ, + 0, + null, + std.os.windows.OPEN_EXISTING, + 0, + null, + ); + _ = SetStdHandle(handle_identifiers[fd_i], handles[fd_i].*); + } + } + + bun.win32.STDERR_FD = if (stderr != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stderr) else bun.invalid_fd; + bun.win32.STDOUT_FD = if (stdout != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdout) else bun.invalid_fd; + bun.win32.STDIN_FD = if (stdin != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdin) else bun.invalid_fd; + + buffered_stdin.unbuffered_reader.context.handle = bun.win32.STDIN_FD; + + // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp + const CP_UTF8 = 65001; + console_output_codepage = w.kernel32.GetConsoleOutputCP(); + _ = w.kernel32.SetConsoleOutputCP(CP_UTF8); + + console_codepage = w.kernel32.GetConsoleOutputCP(); + _ = SetConsoleCP(CP_UTF8); + + const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200; + const ENABLE_PROCESSED_OUTPUT = 0x0001; + + var mode: w.DWORD = undefined; + if (w.kernel32.GetConsoleMode(stdin, &mode) != 0) { + console_mode[0] = mode; + bun_stdio_tty[0] = 1; + _ = SetConsoleMode(stdin, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); + } + + if (w.kernel32.GetConsoleMode(stdout, &mode) != 0) { + console_mode[1] = mode; + bun_stdio_tty[1] = 1; + _ = SetConsoleMode(stdout, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + } + + if (w.kernel32.GetConsoleMode(stderr, &mode) != 0) { + console_mode[2] = mode; + bun_stdio_tty[2] = 1; + _ = SetConsoleMode(stderr, ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + } + } + }; + + pub const Stdio = struct { + pub fn init() void { + bun.C.bun_initialize_process(); + + if (Environment.isWindows) { + WindowsStdio.init(); + } + + const stdout = bun.sys.File.from(std.io.getStdOut()); + const stderr = bun.sys.File.from(std.io.getStdErr()); + var output_source = Output.Source.init(stdout, stderr); + + output_source.set(); + + if (comptime Environment.isDebug) { + initScopedDebugWriterAtStartup(); + } + } + + pub fn restore() void { + if (Environment.isWindows) { + WindowsStdio.restore(); + } else { + bun.C.bun_restore_stdio(); + } + } + }; + pub fn set(_source: *Source) void { source = _source.*; @@ -147,12 +273,12 @@ pub const Source = struct { enable_color = false; } - const is_stdout_tty = _source.stream.isTty(); + const is_stdout_tty = bun_stdio_tty[1] != 0; if (is_stdout_tty) { stdout_descriptor_type = OutputStreamDescriptor.terminal; } - const is_stderr_tty = _source.error_stream.isTty(); + const is_stderr_tty = bun_stdio_tty[2] != 0; if (is_stderr_tty) { stderr_descriptor_type = OutputStreamDescriptor.terminal; } diff --git a/src/panic_handler.zig b/src/panic_handler.zig index 2f73aeb40d..6868e13b87 100644 --- a/src/panic_handler.zig +++ b/src/panic_handler.zig @@ -39,6 +39,8 @@ pub fn NewPanicHandler(comptime panic_func: fn ([]const u8, ?*std.builtin.StackT Output.disableBuffering(); + Output.Source.Stdio.restore(); + if (bun.auto_reload_on_crash) { // attempt to prevent a double panic bun.auto_reload_on_crash = false; diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 5d911258c9..e0ce6aaa44 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -7869,7 +7869,7 @@ it("should install correct version of peer dependency from root package", async describe("Registry URLs", () => { // Some of the non failing URLs are invalid, but bun's URL parser ignores // the validation error and returns a valid serialized URL anyway. - const registryURLs: [url: string, fails: boolean][] = [ + const registryURLs: [url: string, fails: boolean | -1][] = [ ["asdfghjklqwertyuiop", true], [" ", true], ["::::::::::::::::", true], @@ -7888,7 +7888,7 @@ describe("Registry URLs", () => { ["https://example.com/[]?[]#[]", false], ["http://example/%?%#%", false], ["c:", true], - ["c:/", false], + ["c:/", -1], ["http://點看", false], // gets converted to punycode ["http://xn--c1yn36f/", false], ]; @@ -7927,7 +7927,10 @@ describe("Registry URLs", () => { expect(stderr).toBeDefined(); const err = await new Response(stderr).text(); - if (fails) { + if (fails === -1) { + expect(err).toContain(`Registry URL must be http:// or https://`); + expect(err).toContain("error: InvalidURL"); + } else if (fails) { expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); expect(err).toContain("error: InvalidURL"); } else {