diff --git a/packages/bun-usockets/src/eventing/libuv.c b/packages/bun-usockets/src/eventing/libuv.c index c1db86c431..c7625114b6 100644 --- a/packages/bun-usockets/src/eventing/libuv.c +++ b/packages/bun-usockets/src/eventing/libuv.c @@ -198,7 +198,7 @@ void us_loop_free(struct us_loop_t *loop) { void us_loop_run(struct us_loop_t *loop) { us_loop_integrate(loop); - uv_run(loop->uv_loop, UV_RUN_NOWAIT); + uv_run(loop->uv_loop, UV_RUN_ONCE); } struct us_poll_t *us_create_poll(struct us_loop_t *loop, int fallthrough, @@ -327,11 +327,11 @@ void us_internal_async_wakeup(struct us_internal_async *a) { uv_async_send(uv_async); } -int us_socket_get_error(int ssl, struct us_socket_t* s) -{ +int us_socket_get_error(int ssl, struct us_socket_t *s) { int error = 0; socklen_t len = sizeof(error); - if (getsockopt(us_poll_fd((struct us_poll_t*)s), SOL_SOCKET, SO_ERROR, (char*)&error, &len) == -1) { + if (getsockopt(us_poll_fd((struct us_poll_t *)s), SOL_SOCKET, SO_ERROR, + (char *)&error, &len) == -1) { return errno; } return error; diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index a1c36bd840..1781799e79 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -3076,7 +3076,7 @@ pub export fn Bun__escapeHTML16(globalObject: *JSC.JSGlobalObject, input_value: std.debug.assert( std.mem.eql( u16, - (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false) catch unreachable).?, + (strings.toUTF16Alloc(bun.default_allocator, strings.toUTF8Alloc(bun.default_allocator, escaped_html) catch unreachable, false, false) catch unreachable).?, escaped_html, ), ); diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig index 7985273b4f..d2be93a941 100644 --- a/src/bun.js/api/glob.zig +++ b/src/bun.js/api/glob.zig @@ -55,23 +55,15 @@ const ScanOpts = struct { } // Conver to utf-16 - const utf16 = (bun.strings.toUTF16Alloc( + const utf16 = bun.strings.toUTF16AllocForReal( allocator, cwd_zig_str.slice(), // Let windows APIs handle errors with invalid surrogate pairs, etc. false, + false, ) catch { globalThis.throwOutOfMemory(); return null; - }) orelse brk: { - // All ascii - const output = allocator.alloc(u16, cwd_zig_str.len) catch { - globalThis.throwOutOfMemory(); - return null; - }; - - bun.strings.copyU8IntoU16(output, cwd_zig_str.slice()); - break :brk output; }; const ptr: [*]u8 = @ptrCast(utf16.ptr); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index c7fb8c0d07..369ab361de 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -121,7 +121,7 @@ pub const ZigString = extern struct { } pub fn dupeForJS(utf8: []const u8, allocator: std.mem.Allocator) !ZigString { - if (try strings.toUTF16Alloc(allocator, utf8, false)) |utf16| { + if (try strings.toUTF16Alloc(allocator, utf8, false, false)) |utf16| { var out = ZigString.init16(utf16); out.mark(); out.markUTF16(); diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index 761f426365..a5912b148a 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -225,16 +225,14 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { self.first = false; if (io.Information == 0) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = 0", .{ - @intFromPtr(self.dir.fd), - }); + bun.sys.syslog("NtQueryDirectoryFile({}) = 0", .{bun.toFD(self.dir.fd)}); return .{ .result = null }; } self.index = 0; self.end_index = io.Information; // If the handle is not a directory, we'll get STATUS_INVALID_PARAMETER. if (rc == .INVALID_PARAMETER) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); return .{ .err = .{ .errno = @intFromEnum(bun.C.SystemErrno.ENOTDIR), @@ -244,13 +242,13 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { } if (rc == .NO_MORE_FILES) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); self.end_index = self.index; return .{ .result = null }; } if (rc != .SUCCESS) { - bun.sys.syslog("NtQueryDirectoryFile({d}) = {s}", .{ @intFromPtr(self.dir.fd), @tagName(rc) }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {s}", .{ bun.toFD(self.dir.fd), @tagName(rc) }); if ((bun.windows.Win32Error.fromNTStatus(rc).toSystemErrno())) |errno| { return .{ @@ -269,7 +267,7 @@ pub fn NewIterator(comptime use_windows_ospath: bool) type { }; } - bun.sys.syslog("NtQueryDirectoryFile({d}) = {d}", .{ @intFromPtr(self.dir.fd), self.end_index }); + bun.sys.syslog("NtQueryDirectoryFile({}) = {d}", .{ bun.toFD(self.dir.fd), self.end_index }); } const dir_info: *w.FILE_DIRECTORY_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 85a7dc2c87..394c544112 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -3635,7 +3635,10 @@ pub const Blob = struct { if (could_be_all_ascii == null or !could_be_all_ascii.?) { // if toUTF16Alloc returns null, it means there are no non-ASCII characters // instead of erroring, invalid characters will become a U+FFFD replacement character - if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| { + if (strings.toUTF16Alloc(bun.default_allocator, buf, false, false) catch { + global.throwOutOfMemory(); + return .zero; + }) |external| { if (lifetime != .temporary) this.setIsASCIIFlag(false); @@ -3735,7 +3738,7 @@ pub const Blob = struct { var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); // if toUTF16Alloc returns null, it means there are no non-ASCII characters - if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| { + if (strings.toUTF16Alloc(allocator, buf, false, false) catch null) |external| { if (comptime lifetime != .temporary) this.setIsASCIIFlag(false); const result = ZigString.init16(external).toJSONObject(global); allocator.free(external); @@ -4393,7 +4396,7 @@ pub const InternalBlob = struct { pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue { const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items); - if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false) catch &[_]u16{}) |out| { + if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false, false) catch &[_]u16{}) |out| { const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis); return_value.ensureStillAlive(); this.deinit(); diff --git a/src/bun.js/webcore/encoding.zig b/src/bun.js/webcore/encoding.zig index 7c87810c06..50b1737953 100644 --- a/src/bun.js/webcore/encoding.zig +++ b/src/bun.js/webcore/encoding.zig @@ -641,7 +641,7 @@ pub const TextDecoder = struct { buffer_slice; if (this.fatal) { - if (toUTF16(default_allocator, moved_buffer_slice_8, true)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, true, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -660,7 +660,7 @@ pub const TextDecoder = struct { } } } else { - if (toUTF16(default_allocator, moved_buffer_slice_8, false)) |result_| { + if (toUTF16(default_allocator, moved_buffer_slice_8, false, false)) |result_| { if (result_) |result| { return ZigString.toExternalU16(result.ptr, result.len, globalThis); } @@ -899,7 +899,7 @@ pub const Encoder = struct { return bun.String.createExternalGloballyAllocated(.latin1, input); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(bun.default_allocator, input, false) catch return bun.String.dead; + const converted = strings.toUTF16Alloc(bun.default_allocator, input, false, false) catch return bun.String.dead; if (converted) |utf16| { defer bun.default_allocator.free(input); return bun.String.createExternalGloballyAllocated(.utf16, utf16); @@ -978,7 +978,7 @@ pub const Encoder = struct { return str.toJS(global); }, .buffer, .utf8 => { - const converted = strings.toUTF16Alloc(allocator, input, false) catch return ZigString.init("Out of memory").toErrorInstance(global); + const converted = strings.toUTF16Alloc(allocator, input, false, false) catch return ZigString.init("Out of memory").toErrorInstance(global); if (converted) |utf16| { return ZigString.toExternalU16(utf16.ptr, utf16.len, global); } diff --git a/src/bun.zig b/src/bun.zig index 65c3495778..aebeb4f2ac 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -103,6 +103,14 @@ pub const FileDescriptor = enum(FileDescriptorInt) { try FDImpl.format(FDImpl.decode(fd), fmt_, options_, writer); } + pub fn assertValid(fd: FileDescriptor) void { + FDImpl.decode(fd).assertValid(); + } + + pub fn isValid(fd: FileDescriptor) bool { + return FDImpl.decode(fd).isValid(); + } + pub fn assertKind(fd: FileDescriptor, kind: FDImpl.Kind) void { std.debug.assert(FDImpl.decode(fd).kind == kind); } diff --git a/src/child_process_windows.zig b/src/child_process_windows.zig new file mode 100644 index 0000000000..452bd1ee37 --- /dev/null +++ b/src/child_process_windows.zig @@ -0,0 +1,743 @@ +//! 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.zig b/src/cli.zig index 3061942c3f..a6bd336d3f 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -196,7 +196,10 @@ pub const Arguments = struct { const run_only_params = [_]ParamType{ clap.parseParam("--silent Don't print the script command") catch unreachable, clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable, - }; + } ++ if (Environment.isWindows) [_]ParamType{ + // clap.parseParam("--native-shell Use cmd.exe to interpret package.json scripts") catch unreachable, + clap.parseParam("--no-native-shell Use Bun shell (TODO: flip this switch)") catch unreachable, + } else .{}; pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; const bunx_commands = [_]ParamType{ @@ -835,6 +838,13 @@ pub const Arguments = struct { if (output_file != null) ctx.debug.output_file = output_file.?; + if (cmd == .RunCommand) { + ctx.debug.use_native_shell = if (Environment.isWindows) + !args.flag("--no-native-shell") + else + true; + } + return opts; } }; @@ -1028,6 +1038,8 @@ pub const Command = struct { offline_mode_setting: ?Bunfig.OfflineMode = null, run_in_bun: bool = false, loaded_bunfig: bool = false, + /// Disables using bun.shell.Interpreter for `bun run`, instead spawning cmd.exe + use_native_shell: bool = false, // technical debt macros: MacroOptions = MacroOptions.unspecified, diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 4bf74f71df..711277a8b2 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -142,6 +142,7 @@ pub const BunxCommand = struct { /// Check the enclosing package.json for a matching "bin" /// If not found, check bunx cache dir fn getBinName(bundler: *bun.Bundler, toplevel_fd: bun.FileDescriptor, tempdir_name: []const u8, package_name: []const u8) error{ NoBinFound, NeedToInstall }![]const u8 { + toplevel_fd.assertValid(); return getBinNameFromProjectDirectory(bundler, toplevel_fd, package_name) catch |err| { if (err == error.NoBinFound) { return error.NoBinFound; @@ -323,13 +324,13 @@ pub const BunxCommand = struct { if (PATH.len > 0) { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin:{s}", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin:{s}"), .{ temp_dir, package_fmt, PATH }, ); } else { PATH = try std.fmt.allocPrint( ctx.allocator, - "{s}/{s}--bunx/node_modules/.bin", + bun.pathLiteral("{s}/{s}--bunx/node_modules/.bin"), .{ temp_dir, package_fmt }, ); } @@ -337,7 +338,7 @@ pub const BunxCommand = struct { const bunx_cache_dir = PATH[0 .. temp_dir.len + "/--bunx".len + package_fmt.len]; var absolute_in_cache_dir_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}", .{ bunx_cache_dir, initial_bin_name }) catch unreachable; + var absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}"), .{ bunx_cache_dir, initial_bin_name }) catch unreachable; const passthrough = passthrough_list.items; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index d4d58d0c20..eabf004d4d 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -271,6 +271,7 @@ pub const RunCommand = struct { env: *DotEnv.Loader, passthrough: []const string, silent: bool, + use_native_shell: bool, ) !bool { const shell_bin = findShell(env.map.get("PATH") orelse "", cwd) orelse return error.MissingShell; @@ -303,6 +304,28 @@ pub const RunCommand = struct { combined_script = combined_script_buf; } + if (Environment.isWindows and !use_native_shell) { + if (!silent) { + if (Environment.isDebug) { + Output.prettyError("[bun shell] ", .{}); + } + Output.prettyErrorln("$ {s}", .{combined_script}); + Output.flush(); + } + + const mini = bun.JSC.MiniEventLoop.initGlobal(env); + bun.shell.InterpreterMini.initAndRunFromSource(mini, name, combined_script) catch |err| { + if (!silent) { + Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); + } + + Output.flush(); + Global.exit(1); + }; + + return true; + } + var argv = [_]string{ shell_bin, if (Environment.isWindows) "/c" else "-c", @@ -324,7 +347,13 @@ pub const RunCommand = struct { child_process.stdin_behavior = .Inherit; child_process.stdout_behavior = .Inherit; - const result = child_process.spawnAndWait() catch |err| { + if (Environment.isWindows) { + try @import("../child_process_windows.zig").spawnWindows(&child_process); + } else { + try child_process.spawn(); + } + + const result = child_process.wait() catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -1249,6 +1278,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } @@ -1262,6 +1292,7 @@ pub const RunCommand = struct { this_bundler.env, passthrough, ctx.debug.silent, + ctx.debug.use_native_shell, )) return false; temp_script_buffer[0.."post".len].* = "post".*; @@ -1275,6 +1306,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, + ctx.debug.use_native_shell, )) { return false; } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 281ad7b948..c9b31a8ba5 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2540,11 +2540,10 @@ pub const UVLoop = extern struct { pub fn run(this: *UVLoop) void { us_loop_run(this); } - pub const tick = run; - pub fn wait(this: *UVLoop) void { - us_loop_run(this); - } + // TODO: remove these two aliases + pub const tick = run; + pub const wait = run; pub fn inc(this: *UVLoop) void { this.uv_loop.inc(); diff --git a/src/fd.zig b/src/fd.zig index aa18bcf160..162a08328b 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -20,8 +20,12 @@ fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt { return handle; } } + fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System { if (env.os == .windows) { + if (!@inComptime()) { + std.debug.assert(handle != FDImpl.invalid_value); + } return @ptrFromInt(handle); } else { return handle; @@ -122,7 +126,17 @@ pub const FDImpl = packed struct { } pub fn isValid(this: FDImpl) bool { - return this.value.as_system != invalid_value; + return switch (env.os) { + // the 'zero' value on posix is debatable. it can be standard in. + // TODO(@paperdave): steamroll away every use of bun.FileDescriptor.zero + else => this.value.as_system != invalid_value, + .windows => switch (this.kind) { + // zero is not allowed in addition to the invalid value (zero would be a null ptr) + .system => this.value.as_system != invalid_value and this.value.as_system != 0, + // the libuv tag is always fine + .uv => true, + }, + }; } /// When calling this function, you may not be able to close the returned fd. @@ -323,4 +337,8 @@ pub const FDImpl = packed struct { }, } } + + pub fn assertValid(this: FDImpl) void { + std.debug.assert(this.isValid()); + } }; diff --git a/src/http.zig b/src/http.zig index 42529d43b6..82be549922 100644 --- a/src/http.zig +++ b/src/http.zig @@ -799,8 +799,13 @@ pub const HTTPThread = struct { } fn processEvents(this: *@This()) noreturn { - if (comptime Environment.isPosix) + if (comptime Environment.isPosix) { this.loop.num_polls = @max(2, this.loop.num_polls); + } else if (comptime Environment.isWindows) { + this.loop.inc(); + } else { + @compileError("TODO:"); + } while (true) { this.drainEvents(); @@ -810,7 +815,6 @@ pub const HTTPThread = struct { start_time = std.time.nanoTimestamp(); } Output.flush(); - // TODO(@paperdave): this does not wait any time on windows this.loop.run(); if (comptime Environment.isDebug) { const end = std.time.nanoTimestamp(); diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 06763e3388..03913d50d3 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -1062,7 +1062,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // this function encodes to UTF-16 if > 127 // so we don't need to worry about latin1 non-ascii code points // we avoid trim since we wanna keep the utf8 validation intact - const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true) catch { + const utf16_bytes_ = strings.toUTF16AllocNoTrim(bun.default_allocator, data_, true, false) catch { this.terminate(ErrorCode.invalid_utf8); return; }; diff --git a/src/install/extract_tarball.zig b/src/install/extract_tarball.zig index 7bb2212d0d..8f1931ebab 100644 --- a/src/install/extract_tarball.zig +++ b/src/install/extract_tarball.zig @@ -351,7 +351,6 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD ) catch unreachable; return error.InstallFailed; }; - defer final_dir.close(); // and get the fd path const final_path = bun.getFdPath( diff --git a/src/install/windows-shim/BinLinkingShim.zig b/src/install/windows-shim/BinLinkingShim.zig index a1a8465e98..8cb4c552e9 100644 --- a/src/install/windows-shim/BinLinkingShim.zig +++ b/src/install/windows-shim/BinLinkingShim.zig @@ -26,23 +26,33 @@ bin_path: []const u16, /// Information found within the target file's shebang shebang: ?Shebang, +/// Random numbers are chosen for validation purposes +/// These arbitrary numbers will probably not show up in the other fields. +/// This will reveal off-by-one mistakes. +pub const VersionFlag = enum(u13) { + pub const current = .v2; + + v1 = 5474, + v2 = 5475, + _, +}; + pub const Flags = packed struct(u16) { - // the shim doesnt use this right now + // this is set if the shebang content is "node" or "bun" is_node_or_bun: bool, // this is for validation that the shim is not corrupt and to detect offset memory reads - // if this format is ever modified, we will set this flag to false to indicate version 2+ - is_version_1: bool = true, + is_valid: bool = true, // indicates if a shebang is present has_shebang: bool, - // this is for validation that the shim is not corrupt and to detect offset memory reads - must_be_5474: u13 = 5474, + + version_tag: VersionFlag = VersionFlag.current, pub fn isValid(flags: Flags) bool { const mask: u16 = @bitCast(Flags{ .is_node_or_bun = false, - .is_version_1 = true, + .is_valid = true, .has_shebang = false, - .must_be_5474 = std.math.maxInt(u13), + .version_tag = @enumFromInt(std.math.maxInt(u13)), }); const compare_to: u16 = @bitCast(Flags{ diff --git a/src/install/windows-shim/build.zig b/src/install/windows-shim/build.zig index 48e93da2e9..0f369f33bc 100644 --- a/src/install/windows-shim/build.zig +++ b/src/install/windows-shim/build.zig @@ -34,11 +34,7 @@ pub fn build(b: *std.Build) void { .optimize = .Debug, .use_llvm = true, .use_lld = true, - .unwind_tables = false, - .omit_frame_pointer = true, - .strip = true, .linkage = .static, - .sanitize_thread = false, .single_threaded = true, .link_libc = false, }); diff --git a/src/install/windows-shim/bun_shim_impl.exe b/src/install/windows-shim/bun_shim_impl.exe index ded01c18a8..8ccadc8941 100755 Binary files a/src/install/windows-shim/bun_shim_impl.exe and b/src/install/windows-shim/bun_shim_impl.exe differ diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index 5f84407afd..6e2e02d4f9 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -34,7 +34,8 @@ //! Prior Art: //! - https://github.com/ScoopInstaller/Shim/blob/master/src/shim.cs //! -//! The compiled binary is 10240 bytes and is `@embedFile`d into Bun itself +//! The compiled binary is 10240 bytes and is `@embedFile`d into Bun itself. +//! When this file is updated, the new binary should be compiled and BinLinkingShim.VersionFlag.current should be updated. const std = @import("std"); const builtin = @import("builtin"); @@ -387,6 +388,10 @@ inline fn launcher(bun_ctx: anytype) noreturn { std.debug.assert(user_arguments_u8.len != 2); std.debug.assert(user_arguments_u8.len == 0 or user_arguments_u8[0] == ' '); + // TODO(@paperdave): this explanation is incorrect. there are two off-by-four bugs. + // both in the opposite direction, so it ends up working out. later, the code needs + // to be simplified to explicitly take advantage of this. + // // Read the metadata file into the memory right after the image path. // // i'm really proud of this technique, because it will create an absolute path, but @@ -395,12 +400,17 @@ inline fn launcher(bun_ctx: anytype) noreturn { // we do this by reusing the memory in the first buffer // BUF1: '\??\C:\Users\dave\project\node_modules\.bin\hello.bunx!!!!!!!!!!!!!!!!!!!!!!' // ^^ ^ ^ - // S| | image_path_b_len + // S| | image_path_b_len + nt_object_prefix.len // | 'ptr' initial value // the read ptr var read_ptr = brk: { - var left = image_path_b_len / 2 - (if (is_standalone) 2 * ".exe".len else ".bunx".len); - var ptr: [*]u16 = buf1_u16[left..]; + var left = image_path_b_len / 2 - (if (is_standalone) 2 * "exe".len else "bunx".len) + 1; + var ptr: [*]u16 = buf1_u16[nt_object_prefix.len + left ..]; + + // if this is false, potential out of bounds memory access + std.debug.assert(@intFromPtr(ptr) - left * @sizeOf(std.meta.Child(@TypeOf(ptr))) >= @intFromPtr(buf1_u16)); + std.debug.assert(ptr[1] == '.'); + inline for (0..1) |_| { while (true) { if (ptr[0] == '\\') { diff --git a/src/js_ast.zig b/src/js_ast.zig index 05b7aeb9ae..8672c28dc3 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2266,7 +2266,7 @@ pub const E = struct { if (s.isUTF8()) { if (comptime !Environment.isNative) { - const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false) catch return 0) orelse return s.data.len; + const allocated = (strings.toUTF16Alloc(bun.default_allocator, s.data, false, false) catch return 0) orelse return s.data.len; defer bun.default_allocator.free(allocated); return @as(u32, @truncate(allocated.len)); } diff --git a/src/libarchive/libarchive.zig b/src/libarchive/libarchive.zig index a5e2f10db5..9226c817e5 100644 --- a/src/libarchive/libarchive.zig +++ b/src/libarchive/libarchive.zig @@ -490,6 +490,9 @@ pub const Archive = struct { const dir = dir_; const dir_fd = dir.fd; + const loop = if (Environment.isWindows) bun.Async.Loop.get() else {}; + var w_path: if (Environment.isWindows) bun.WPathBuffer else void = undefined; + loop: while (true) { const r = @as(Status, @enumFromInt(lib.archive_read_next_header(archive, &entry))); @@ -577,11 +580,13 @@ pub const Archive = struct { }, Kind.file => { const mode: bun.Mode = if (comptime Environment.isWindows) 0 else @intCast(lib.archive_entry_perm(entry)); - const file = dir.createFileZ(pathname, .{ .truncate = true, .mode = mode }) catch |err| brk: { + const os_path = if (Environment.isWindows) bun.strings.toWPathNormalized(&w_path, pathname) else pathname; + const createFileOS = if (Environment.isWindows) std.fs.Dir.createFileW else std.fs.Dir.createFileZ; + const file = createFileOS(dir, os_path, .{ .truncate = true, .mode = mode }) catch |err| brk: { switch (err) { error.AccessDenied, error.FileNotFound => { dir.makePath(std.fs.path.dirname(slice) orelse return err) catch {}; - break :brk try dir.createFileZ(pathname, .{ + break :brk try createFileOS(dir, os_path, .{ .truncate = true, .mode = mode, }); @@ -593,9 +598,19 @@ pub const Archive = struct { }; const file_handle = bun.toLibUVOwnedFD(file.handle); - defer { - if (comptime close_handles) _ = bun.sys.close(file_handle); - } + defer if (comptime close_handles) { + if (Environment.isWindows) { + // Using Async.Closer defers closing the file to a different thread. + // On windows, AV hangs these closes really badly. + // + // 'bun i @mui/icons-material' takes like 20 seconds to extract + // + // The install still takes a long time but this makes it a little bit better. + bun.Async.Closer.close(bun.uvfdcast(file_handle), loop); + } else { + _ = bun.sys.close(file_handle); + } + }; const entry_size = @max(lib.archive_entry_size(entry), 0); const size = @as(usize, @intCast(entry_size)); diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 09f27a7538..13d928d2dc 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -1024,6 +1024,7 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { defer file.close(); break :src try file.reader().readAllAlloc(arena.allocator(), std.math.maxInt(u32)); }; + defer arena.deinit(); const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; @@ -1067,6 +1068,52 @@ pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); } + pub fn initAndRunFromSource(mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8) !void { + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + + const jsobjs: []JSValue = &[_]JSValue{}; + var out_parser: ?bun.shell.Parser = null; + var out_lex_result: ?bun.shell.LexResult = null; + const script = ThisInterpreter.parse(&arena, src, jsobjs, &out_parser, &out_lex_result) catch |err| { + if (err == bun.shell.ParseError.Lex) { + std.debug.assert(out_lex_result != null); + const str = out_lex_result.?.combineErrors(arena.allocator()); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, str }); + bun.Global.exit(1); + } + + if (out_parser) |*p| { + const errstr = p.combineErrors(); + bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, errstr }); + bun.Global.exit(1); + } + + return err; + }; + const script_heap = try arena.allocator().create(ast.Script); + script_heap.* = script; + var interp = switch (ThisInterpreter.init(mini, bun.default_allocator, &arena, script_heap, jsobjs)) { + .err => |e| { + GlobalHandle.init(mini).actuallyThrow(e); + return; + }, + .result => |i| i, + }; + const IsDone = struct { + done: bool = false, + + fn isDone(this: *anyopaque) bool { + const asdlfk = bun.cast(*const @This(), this); + return asdlfk.done; + } + }; + var is_done: IsDone = .{}; + interp.done = &is_done.done; + try interp.run(); + mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); + } + pub fn run(this: *ThisInterpreter) !void { var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_shell.io); this.started.store(true, .SeqCst); diff --git a/src/string.zig b/src/string.zig index c70920715e..2a1bbf5539 100644 --- a/src/string.zig +++ b/src/string.zig @@ -1270,7 +1270,7 @@ pub const SliceWithUnderlyingString = struct { } if (this.utf8.allocator.get()) |_| { - if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false) catch null) |utf16| { + if (bun.strings.toUTF16Alloc(bun.default_allocator, this.utf8.slice(), false, false) catch null) |utf16| { this.utf8.deinit(); this.utf8 = .{}; return JSC.ZigString.toExternalU16(utf16.ptr, utf16.len, globalObject); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 7b7e8bf4cf..d3337312df 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1316,7 +1316,7 @@ pub fn withoutUTF8BOM(bytes: []const u8) []const u8 { /// Convert a UTF-8 string to a UTF-16 string IF there are any non-ascii characters /// If there are no non-ascii characters, this returns null /// This is intended to be used for strings that go to JavaScript -pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) ?[:0]u16 else ?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const trimmed = bun.simdutf.trim.utf8(bytes); @@ -1329,11 +1329,15 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa if (out_length == 0) break :simd null; - var out = try allocator.alloc(u16, out_length); + var out = try allocator.alloc(u16, out_length + if (sentinel) 1 else 0); log("toUTF16 {d} UTF8 -> {d} UTF16", .{ bytes.len, out_length }); const res = bun.simdutf.convert.utf8.to.utf16.with_errors.le(trimmed, out); if (res.status == .success) { + if (comptime sentinel) { + out[out_length] = 0; + return out[0 .. out_length + 1 :0]; + } return out; } @@ -1429,13 +1433,33 @@ pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fa strings.copyU8IntoU16(output.items[output.items.len - remaining.len ..], remaining); } + if (comptime sentinel) { + output.items[output.items.len] = 0; + return output.items[0 .. output.items.len + 1 :0]; + } + return output.items; } return null; } -pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 { +// this one does the thing it's named after +pub fn toUTF16AllocForReal(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime sentinel: bool) !if (sentinel) [:0]u16 else []u16 { + return (try toUTF16Alloc(allocator, bytes, fail_if_invalid, sentinel)) orelse { + const output = try allocator.alloc(u16, bytes.len + if (sentinel) 1 else 0); + bun.strings.copyU8IntoU16(output, bytes); + + if (comptime sentinel) { + output[bytes.len] = 0; + return output[0..bytes.len :0]; + } + + return output; + }; +} + +pub fn toUTF16AllocNoTrim(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool, comptime _: bool) !?[]u16 { if (strings.firstNonASCII(bytes)) |i| { const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: { const out_length = bun.simdutf.length.utf16.from.utf8(bytes); @@ -5256,34 +5280,37 @@ pub fn concatIfNeeded( std.debug.assert(remain.len == 0); } +/// This will simply ignore invalid UTF-8 and just do it pub fn convertUTF8toUTF16InBuffer( buf: []u16, input: []const u8, ) []u16 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); - - const result = bun.simdutf.convert.utf8.to.utf16.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF8toUTF16"), - } + // TODO(@paperdave): implement error handling here. + // for now this will cause invalid utf-8 to be ignored and become empty. + // this is lame because of https://github.com/oven-sh/bun/issues/8197 + // it will cause process.env.whatever to be len=0 instead of the data + // but it's better than failing the run entirely + // + // the reason i didn't implement the fallback is purely because our + // code in this file is too chaotic. it is left as a TODO + const result = bun.simdutf.convert.utf8.to.utf16.le(input, buf); + return buf[0..result]; } pub fn convertUTF16toUTF8InBuffer( buf: []u8, input: []const u16, ) ![]const u8 { - if (!Environment.isWindows) @compileError("please dont't use this function on posix until fixing the todos."); + // See above - const result = bun.simdutf.convert.utf16.to.utf8.with_errors.le(input, buf); - switch (result.status) { - .success => return buf[0..result.count], - // TODO(@paperdave): handle surrogate - .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), - else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), - } + const result = bun.simdutf.convert.utf16.to.utf8.le(input, buf); + // switch (result.status) { + // .success => return buf[0..result.count], + // // TODO(@paperdave): handle surrogate + // .surrogate => @panic("TODO: handle surrogate in convertUTF8toUTF16"), + // else => @panic("TODO: handle error in convertUTF16toUTF8InBuffer"), + // } + return buf[0..result]; } pub inline fn charIsAnySlash(char: u8) bool {