diff --git a/.github/workflows/build-darwin.yml b/.github/workflows/build-darwin.yml index d35edfab451316..bbeb946bde30f3 100644 --- a/.github/workflows/build-darwin.yml +++ b/.github/workflows/build-darwin.yml @@ -27,7 +27,7 @@ on: type: boolean env: - LLVM_VERSION: 17 + LLVM_VERSION: 16 BUN_VERSION: 1.1.2 jobs: @@ -79,7 +79,7 @@ jobs: openssl@1.1 \ ninja \ golang \ - gnu-sed --force + gnu-sed --force --overwrite echo "$(brew --prefix ccache)/bin" >> $GITHUB_PATH echo "$(brew --prefix coreutils)/libexec/gnubin" >> $GITHUB_PATH echo "$(brew --prefix llvm@$LLVM_VERSION)/bin" >> $GITHUB_PATH @@ -136,7 +136,7 @@ jobs: openssl@1.1 \ ninja \ golang \ - gnu-sed --force + gnu-sed --force --overwrite echo "$(brew --prefix ccache)/bin" >> $GITHUB_PATH echo "$(brew --prefix coreutils)/libexec/gnubin" >> $GITHUB_PATH echo "$(brew --prefix llvm@$LLVM_VERSION)/bin" >> $GITHUB_PATH @@ -218,7 +218,7 @@ jobs: openssl@1.1 \ ninja \ golang \ - gnu-sed --force + gnu-sed --force --overwrite echo "$(brew --prefix ccache)/bin" >> $GITHUB_PATH echo "$(brew --prefix coreutils)/libexec/gnubin" >> $GITHUB_PATH echo "$(brew --prefix llvm@$LLVM_VERSION)/bin" >> $GITHUB_PATH diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 3ed47a4c394bf7..956d6feed382d0 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -28,7 +28,7 @@ on: env: # Must specify exact version of LLVM for Windows - LLVM_VERSION: 17.0.6 + LLVM_VERSION: 16.0.6 BUN_VERSION: 1.1.2 jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16ea3d011f115f..7bfe1e28e15b9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64 arch: x64 cpu: haswell @@ -92,7 +92,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64-baseline arch: x64 cpu: nehalem @@ -103,7 +103,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-12' }} tag: darwin-aarch64 arch: aarch64 cpu: native @@ -172,7 +172,7 @@ jobs: with: run-id: ${{ inputs.run-id }} pr-number: ${{ github.event.number }} - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64 darwin-x64-baseline-test: if: ${{ inputs.run-id && always() || github.event_name == 'pull_request' }} @@ -183,7 +183,7 @@ jobs: with: run-id: ${{ inputs.run-id }} pr-number: ${{ github.event.number }} - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64-baseline darwin-aarch64-test: if: ${{ inputs.run-id && always() || github.event_name == 'pull_request' }} @@ -194,7 +194,7 @@ jobs: with: run-id: ${{ inputs.run-id }} pr-number: ${{ github.event.number }} - runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-12' }} tag: darwin-aarch64 windows-x64-test: if: ${{ inputs.run-id && always() || github.event_name == 'pull_request' }} diff --git a/.github/workflows/create-release-build.yml b/.github/workflows/create-release-build.yml index affb2c40e012df..3a2f797649ee8b 100644 --- a/.github/workflows/create-release-build.yml +++ b/.github/workflows/create-release-build.yml @@ -85,7 +85,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64 arch: x64 cpu: haswell @@ -95,7 +95,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-13-large' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'macos-12-large' || 'macos-12' }} tag: darwin-x64-baseline arch: x64 cpu: nehalem @@ -105,7 +105,7 @@ jobs: uses: ./.github/workflows/build-darwin.yml secrets: inherit with: - runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-13' }} + runs-on: ${{ github.repository_owner == 'oven-sh' && 'namespace-profile-bun-ci-darwin-aarch64' || 'macos-12' }} tag: darwin-aarch64 arch: aarch64 cpu: native diff --git a/CMakeLists.txt b/CMakeLists.txt index 71771ca10e6292..306484ab3cd9e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0067 NEW) set(Bun_VERSION "1.1.5") -set(WEBKIT_TAG 2ce7bb4314c516d9c7ba12be3581f6a986013781) +set(WEBKIT_TAG 5fe78270f2efca3c0ee3013259776923053d5011) set(BUN_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}") message(STATUS "Configuring Bun ${Bun_VERSION} in ${BUN_WORKDIR}") @@ -107,7 +107,7 @@ endif() # we do some extra work afterwards to double-check, and we will rerun BUN_FIND_LLVM if the compiler did not match. # # If the user passes -DLLVM_PREFIX, most of this logic is skipped, but we still warn if invalid. -set(LLVM_VERSION 17) +set(LLVM_VERSION 16) macro(BUN_FIND_LLVM) find_program( @@ -1119,7 +1119,6 @@ if(APPLE) target_link_options(${bun} PUBLIC "-dead_strip") target_link_options(${bun} PUBLIC "-dead_strip_dylibs") target_link_options(${bun} PUBLIC "-Wl,-stack_size,0x1200000") - target_link_options(${bun} PUBLIC "-ld64") # fixes macOS x64 linking issue target_link_options(${bun} PUBLIC "-exported_symbols_list" "${BUN_SRC}/symbols.txt") set_target_properties(${bun} PROPERTIES LINK_DEPENDS "${BUN_SRC}/symbols.txt") diff --git a/Dockerfile b/Dockerfile index f36ec9e50a562c..2e65b1a74a0ee5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG ZIG_OPTIMIZE=ReleaseFast ARG CMAKE_BUILD_TYPE=Release ARG NODE_VERSION="20" -ARG LLVM_VERSION="17" +ARG LLVM_VERSION="16" ARG ZIG_VERSION="0.12.0-dev.1828+225fe6ddb" ARG SCCACHE_BUCKET @@ -34,7 +34,7 @@ ARG SCCACHE_ENDPOINT ARG AWS_ACCESS_KEY_ID ARG AWS_SECRET_ACCESS_KEY -FROM bitnami/minideb:bookworm as bun-base +FROM bitnami/minideb:bullseye as bun-base ARG BUN_DOWNLOAD_URL_BASE ARG DEBIAN_FRONTEND @@ -53,7 +53,7 @@ ENV BUN_DEPS_OUT_DIR=${BUN_DEPS_OUT_DIR} ENV CXX=clang++-${LLVM_VERSION} ENV CC=clang-${LLVM_VERSION} -ENV AR=/usr/bin/llvm-ar +ENV AR=/usr/bin/llvm-ar-${LLVM_VERSION} ENV LD=lld-${LLVM_VERSION} ENV SCCACHE_BUCKET=${SCCACHE_BUCKET} @@ -67,11 +67,13 @@ RUN install_packages \ ca-certificates \ curl \ gnupg \ - && echo "deb https://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.list \ - && echo "deb-src https://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-${LLVM_VERSION} main" >> /etc/apt/sources.list.d/llvm.list \ + && echo "deb https://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.list \ + && echo "deb-src https://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" >> /etc/apt/sources.list.d/llvm.list \ && curl -fsSL "https://apt.llvm.org/llvm-snapshot.gpg.key" | apt-key add - \ && echo "deb https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ && curl -fsSL "https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key" | apt-key add - \ + && echo "deb https://apt.kitware.com/ubuntu/ focal main" > /etc/apt/sources.list.d/kitware.list \ + && curl -fsSL "https://apt.kitware.com/keys/kitware-archive-latest.asc" | apt-key add - \ && install_packages \ wget \ bash \ diff --git a/Makefile b/Makefile index ed17bb08c9d724..8cbb4300909596 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,8 @@ ZIG ?= $(shell which zig 2>/dev/null || echo -e "error: Missing zig. Please make # This is easier to happen than you'd expect. # Using realpath here causes issues because clang uses clang++ as a symlink # so if that's resolved, it won't build for C++ -REAL_CC = $(shell which clang-17 2>/dev/null || which clang 2>/dev/null) -REAL_CXX = $(shell which clang++-17 2>/dev/null || which clang++ 2>/dev/null) +REAL_CC = $(shell which clang-16 2>/dev/null || which clang 2>/dev/null) +REAL_CXX = $(shell which clang++-16 2>/dev/null || which clang++ 2>/dev/null) CLANG_FORMAT = $(shell which clang-format-16 2>/dev/null || which clang-format 2>/dev/null) CC = $(REAL_CC) @@ -107,14 +107,14 @@ CC_WITH_CCACHE = $(CCACHE_PATH) $(CC) ifeq ($(OS_NAME),darwin) # Find LLVM ifeq ($(wildcard $(LLVM_PREFIX)),) - LLVM_PREFIX = $(shell brew --prefix llvm@17) + LLVM_PREFIX = $(shell brew --prefix llvm@16) endif ifeq ($(wildcard $(LLVM_PREFIX)),) LLVM_PREFIX = $(shell brew --prefix llvm) endif ifeq ($(wildcard $(LLVM_PREFIX)),) # This is kinda ugly, but I can't find a better way to error :( - LLVM_PREFIX = $(shell echo -e "error: Unable to find llvm. Please run 'brew install llvm@17' or set LLVM_PREFIX=/path/to/llvm") + LLVM_PREFIX = $(shell echo -e "error: Unable to find llvm. Please run 'brew install llvm@16' or set LLVM_PREFIX=/path/to/llvm") endif LDFLAGS += -L$(LLVM_PREFIX)/lib @@ -154,7 +154,7 @@ CMAKE_FLAGS_WITHOUT_RELEASE = -DCMAKE_C_COMPILER=$(CC) \ -DCMAKE_OSX_DEPLOYMENT_TARGET=$(MIN_MACOS_VERSION) \ $(CMAKE_CXX_COMPILER_LAUNCHER_FLAG) \ -DCMAKE_AR=$(AR) \ - -DCMAKE_RANLIB=$(which llvm-17-ranlib 2>/dev/null || which llvm-ranlib 2>/dev/null) + -DCMAKE_RANLIB=$(which llvm-16-ranlib 2>/dev/null || which llvm-ranlib 2>/dev/null) @@ -657,7 +657,7 @@ endif .PHONY: assert-deps assert-deps: @echo "Checking if the required utilities are available..." - @if [ $(CLANG_VERSION) -lt "15" ]; then echo -e "ERROR: clang version >=15 required, found: $(CLANG_VERSION). Install with:\n\n $(POSIX_PKG_MANAGER) install llvm@17"; exit 1; fi + @if [ $(CLANG_VERSION) -lt "15" ]; then echo -e "ERROR: clang version >=15 required, found: $(CLANG_VERSION). Install with:\n\n $(POSIX_PKG_MANAGER) install llvm@16"; exit 1; fi @cmake --version >/dev/null 2>&1 || (echo -e "ERROR: cmake is required."; exit 1) @$(PYTHON) --version >/dev/null 2>&1 || (echo -e "ERROR: python is required."; exit 1) @$(ESBUILD) --version >/dev/null 2>&1 || (echo -e "ERROR: esbuild is required."; exit 1) diff --git a/docs/project/building-windows.md b/docs/project/building-windows.md index c4cd14ec742d7a..e7c6f6b6bf43cd 100644 --- a/docs/project/building-windows.md +++ b/docs/project/building-windows.md @@ -55,7 +55,7 @@ By default, running unverified scripts are blocked. After Visual Studio, you need the following: -- LLVM 17 +- LLVM 16 - Go - Rust - NASM @@ -73,7 +73,7 @@ The Zig compiler is automatically downloaded, installed, and updated by the buil > irm https://get.scoop.sh | iex > scoop install nodejs-lts go rust nasm ruby perl # scoop seems to be buggy if you install llvm and the rest at the same time -> scoop llvm@17.0.6 +> scoop llvm@16.0.6 ``` If you intend on building WebKit locally (optional), you should install these packages: diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 41cb4a2caa51e5..af02e739f9330d 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -53,17 +53,17 @@ $ brew install bun ## Install LLVM -Bun requires LLVM 17/Clang 17 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager: +Bun requires LLVM 16 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager: {% codetabs %} ```bash#macOS (Homebrew) -$ brew install llvm@17 +$ brew install llvm@16 ``` ```bash#Ubuntu/Debian $ # LLVM has an automatic installation script that is compatible with all versions of Ubuntu -$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 17 all +$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 16 all ``` ```bash#Arch @@ -77,17 +77,17 @@ $ sudo dnf install llvm clang lld ``` ```bash#openSUSE Tumbleweed -$ sudo zypper install clang17 lld17 llvm17 +$ sudo zypper install clang16 lld16 llvm16 ``` {% /codetabs %} -If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-17.0.6). +If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.6). -Make sure Clang/LLVM 17 is in your path: +Make sure Clang/LLVM 16 is in your path: ```bash -$ which clang-17 +$ which clang-16 ``` If not, run this to manually add it: @@ -96,13 +96,13 @@ If not, run this to manually add it: ```bash#macOS (Homebrew) # use fish_add_path if you're using fish -# use path+="$(brew --prefix llvm@17)/bin" if you are using zsh -$ export PATH="$(brew --prefix llvm@17)/bin:$PATH" +# use path+="$(brew --prefix llvm@16)/bin" if you are using zsh +$ export PATH="$(brew --prefix llvm@16)/bin:$PATH" ``` ```bash#Arch # use fish_add_path if you're using fish -$ export PATH="$PATH:/usr/lib/llvm17/bin" +$ export PATH="$PATH:/usr/lib/llvm16/bin" ``` {% /codetabs %} @@ -263,7 +263,7 @@ The issue may manifest when initially running `bun setup` as Clang being unable ``` The C++ compiler - "/usr/bin/clang++-17" + "/usr/bin/clang++-16" is not able to compile a simple test program. ``` diff --git a/scripts/setup.sh b/scripts/setup.sh index 91d6db6b0bc338..ddeff6cdbd99d2 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -15,7 +15,7 @@ fail() { printf "${C_RED}setup error${C_RESET}: %s\n" "$@" } -LLVM_VERSION=17 +LLVM_VERSION=16 # this compiler detection could be better # it is copy pasted from ./env.sh diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 87db8f77ac1bd6..042c6b2d3b8310 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -476,11 +476,19 @@ pub const ImportWatcher = union(enum) { dir_fd: StoredFileDescriptorType, package_json: ?*PackageJSON, comptime copy_file_path: bool, - ) !void { - switch (this) { - inline .hot, .watch => |wacher| try wacher.addFile(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path), - else => {}, - } + ) bun.JSC.Maybe(void) { + return switch (this) { + inline .hot, .watch => |watcher| watcher.addFile( + fd, + file_path, + hash, + loader, + dir_fd, + package_json, + copy_file_path, + ), + .none => .{ .result = {} }, + }; } }; @@ -3552,7 +3560,7 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime // - Directories outside the root directory // - Directories inside node_modules if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) { - watch.addDirectory(dir_fd, file_path, GenericWatcher.getHash(file_path), false) catch {}; + _ = watch.addDirectory(dir_fd, file_path, GenericWatcher.getHash(file_path), false); } } @@ -3566,9 +3574,9 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime pub fn onError( _: *@This(), - err: anyerror, + err: bun.sys.Error, ) void { - Output.prettyErrorln("Watcher crashed: {s}", .{@errorName(err)}); + Output.err(@as(bun.C.E, @enumFromInt(err.errno)), "Watcher crashed", .{}); } pub fn getContext(this: *@This()) *@This().Watcher { diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 6173bf4f3557ab..9ef6dbdc43c910 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -518,7 +518,7 @@ pub const RuntimeTranspilerStore = struct { if (input_file_fd != .zero) { if (!is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { should_close_input_file_fd = false; - vm.bun_watcher.addFile( + _ = vm.bun_watcher.addFile( input_file_fd, path.text, hash, @@ -526,7 +526,7 @@ pub const RuntimeTranspilerStore = struct { .zero, package_json, true, - ) catch {}; + ); } } } @@ -541,7 +541,7 @@ pub const RuntimeTranspilerStore = struct { std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { should_close_input_file_fd = false; - vm.bun_watcher.addFile( + _ = vm.bun_watcher.addFile( input_file_fd, path.text, hash, @@ -549,7 +549,7 @@ pub const RuntimeTranspilerStore = struct { .zero, package_json, true, - ) catch {}; + ); } } } @@ -1445,7 +1445,7 @@ pub const ModuleLoader = struct { if (parse_result.input_fd) |fd_| { if (std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { - jsc_vm.bun_watcher.addFile( + _ = jsc_vm.bun_watcher.addFile( fd_, path.text, this.hash, @@ -1453,7 +1453,7 @@ pub const ModuleLoader = struct { .zero, this.package_json, true, - ) catch {}; + ); } } @@ -1690,7 +1690,7 @@ pub const ModuleLoader = struct { if (input_file_fd != .zero) { if (!is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { should_close_input_file_fd = false; - jsc_vm.bun_watcher.addFile( + _ = jsc_vm.bun_watcher.addFile( input_file_fd, path.text, hash, @@ -1698,7 +1698,7 @@ pub const ModuleLoader = struct { .zero, package_json, true, - ) catch {}; + ); } } } @@ -1733,7 +1733,7 @@ pub const ModuleLoader = struct { if (input_file_fd != .zero) { if (!is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { should_close_input_file_fd = false; - jsc_vm.bun_watcher.addFile( + _ = jsc_vm.bun_watcher.addFile( input_file_fd, path.text, hash, @@ -1741,7 +1741,7 @@ pub const ModuleLoader = struct { .zero, package_json, true, - ) catch {}; + ); } } } diff --git a/src/bun.js/node/fs_events.zig b/src/bun.js/node/fs_events.zig index bb087e661a1caa..27132aa773a0c0 100644 --- a/src/bun.js/node/fs_events.zig +++ b/src/bun.js/node/fs_events.zig @@ -8,6 +8,10 @@ const UnboundedQueue = @import("../unbounded_queue.zig").UnboundedQueue; const TaggedPointerUnion = @import("../../tagged_pointer.zig").TaggedPointerUnion; const string = bun.string; +const PathWatcher = @import("./path_watcher.zig").PathWatcher; +const EventType = PathWatcher.EventType; +const Event = bun.JSC.Node.FSWatcher.Event; + pub const CFAbsoluteTime = f64; pub const CFTimeInterval = f64; pub const CFArrayCallBacks = anyopaque; @@ -405,7 +409,8 @@ pub const FSEventsLoop = struct { } } - handle.emit(path, is_file, if (is_rename) .rename else .change); + const event_type: EventType = if (is_rename) .rename else .change; + handle.emit(event_type.toEvent(path), is_file); } handle.flush(); } @@ -580,13 +585,7 @@ pub const FSEventsWatcher = struct { recursive: bool, ctx: ?*anyopaque, - pub const EventType = enum { - rename, - change, - @"error", - }; - - pub const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, event_type: EventType) void; + pub const Callback = PathWatcher.Callback; pub const UpdateEndCallback = *const fn (ctx: ?*anyopaque) void; pub fn init(loop: *FSEventsLoop, path: string, recursive: bool, callback: Callback, updateEnd: UpdateEndCallback, ctx: ?*anyopaque) *FSEventsWatcher { @@ -605,8 +604,8 @@ pub const FSEventsWatcher = struct { return this; } - pub fn emit(this: *FSEventsWatcher, path: string, is_file: bool, event_type: EventType) void { - this.callback(this.ctx, path, is_file, event_type); + pub fn emit(this: *FSEventsWatcher, event: Event, is_file: bool) void { + this.callback(this.ctx, event, is_file); } pub fn flush(this: *FSEventsWatcher) void { diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 2b10ba06db8402..06c5bbbd005a0a 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -5448,26 +5448,24 @@ pub const NodeFS = struct { const path = args.path.sliceZ(inbuf); - const len = switch (Syscall.readlink(path, &outbuf)) { - .err => |err| return .{ - .err = err.withPath(args.path.slice()), - }, - .result => |len| len, + const link_path = switch (Syscall.readlink(path, &outbuf)) { + .err => |err| return .{ .err = err.withPath(args.path.slice()) }, + .result => |result| result, }; return .{ .result = switch (args.encoding) { .buffer => .{ - .buffer = Buffer.fromString(outbuf[0..len], bun.default_allocator) catch unreachable, + .buffer = Buffer.fromString(link_path, bun.default_allocator) catch unreachable, }, else => if (args.path == .slice_with_underlying_string and - strings.eqlLong(args.path.slice_with_underlying_string.slice(), outbuf[0..len], true)) + strings.eqlLong(args.path.slice_with_underlying_string.slice(), link_path, true)) .{ .string = args.path.slice_with_underlying_string.dupeRef(), } else .{ - .string = .{ .utf8 = .{}, .underlying = bun.String.createUTF8(outbuf[0..len]) }, + .string = .{ .utf8 = .{}, .underlying = bun.String.createUTF8(link_path) }, }, }, }; @@ -5919,18 +5917,7 @@ pub const NodeFS = struct { } pub fn watch(_: *NodeFS, args: Arguments.Watch, comptime _: Flavor) Maybe(Return.Watch) { - const watcher = args.createFSWatcher() catch |err| { - const buf = std.fmt.allocPrint(bun.default_allocator, "{s} watching {}", .{ @errorName(err), bun.fmt.QuotedFormatter{ .text = args.path.slice() } }) catch unreachable; - defer bun.default_allocator.free(buf); - args.global_this.throwValue((JSC.SystemError{ - .message = bun.String.init(buf), - .code = bun.String.init(@errorName(err)), - .syscall = bun.String.static("watch"), - .path = bun.String.init(args.path.slice()), - }).toErrorInstance(args.global_this)); - return Maybe(Return.Watch){ .result = JSC.JSValue.undefined }; - }; - return Maybe(Return.Watch){ .result = watcher }; + return args.createFSWatcher(); } pub fn createReadStream(_: *NodeFS, _: Arguments.CreateReadStream, comptime _: Flavor) Maybe(Return.CreateReadStream) { diff --git a/src/bun.js/node/node_fs_watcher.zig b/src/bun.js/node/node_fs_watcher.zig index 5f1ed8f6d7f833..021ded377f2c95 100644 --- a/src/bun.js/node/node_fs_watcher.zig +++ b/src/bun.js/node/node_fs_watcher.zig @@ -17,11 +17,11 @@ const Environment = bun.Environment; const Async = bun.Async; const log = Output.scoped(.@"fs.watch", true); const PathWatcher = if (Environment.isWindows) @import("./win_watcher.zig") else @import("./path_watcher.zig"); + pub const FSWatcher = struct { ctx: *VirtualMachine, verbose: bool = false, - // JSObject mutex: Mutex, signal: ?*JSC.AbortSignal, persistent: bool, @@ -30,13 +30,14 @@ pub const FSWatcher = struct { globalThis: *JSC.JSGlobalObject, js_this: JSC.JSValue, encoding: JSC.Node.Encoding, - // user can call close and pre-detach so we need to track this + + /// User can call close and pre-detach so we need to track this closed: bool, - // While it's not closed, the pending activity + /// While it's not closed, the pending activity pending_activity_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(1), - current_task: FSWatchTask = undefined, + pub usingnamespace JSC.Codegen.JSFSWatcher; pub usingnamespace bun.New(@This()); @@ -63,12 +64,11 @@ pub const FSWatcher = struct { concurrent_task: JSC.ConcurrentTask = undefined, pub const Entry = struct { - file_path: string, - event_type: EventType, + event: Event, needs_free: bool, }; - pub fn append(this: *FSWatchTask, file_path: string, event_type: EventType, needs_free: bool) void { + pub fn append(this: *FSWatchTask, event: Event, needs_free: bool) void { if (this.count == 8) { this.enqueue(); const ctx = this.ctx; @@ -79,8 +79,7 @@ pub const FSWatcher = struct { } this.entries[this.count] = .{ - .file_path = file_path, - .event_type = event_type, + .event = event, .needs_free = needs_free, }; this.count += 1; @@ -90,16 +89,12 @@ pub const FSWatcher = struct { // this runs on JS Context Thread for (this.entries[0..this.count]) |entry| { - switch (entry.event_type) { - .rename => { - this.ctx.emit(entry.file_path, .rename); + switch (entry.event) { + inline .rename, .change => |file_path, t| { + this.ctx.emit(file_path, t); }, - .change => { - this.ctx.emit(entry.file_path, .change); - }, - .@"error" => { - // file_path is the error message in this case - this.ctx.emitError(entry.file_path); + .@"error" => |err| { + this.ctx.emitError(err); }, .abort => { this.ctx.emitIfAborted(); @@ -114,7 +109,7 @@ pub const FSWatcher = struct { } pub fn appendAbort(this: *FSWatchTask) void { - this.append("", .abort, false); + this.append(.abort, false); this.enqueue(); } @@ -134,9 +129,9 @@ pub const FSWatcher = struct { this.cleanEntries(); } pub fn cleanEntries(this: *FSWatchTask) void { - for (this.entries[0..this.count]) |entry| { + for (this.entries[0..this.count]) |*entry| { if (entry.needs_free) { - bun.default_allocator.free(entry.file_path); + entry.event.deinit(); } } this.count = 0; @@ -153,6 +148,36 @@ pub const FSWatcher = struct { } }; + pub const EventPathString = switch (Environment.os) { + .windows => FSWatchTaskWindows.StringOrBytesToDecode, + else => []const u8, + }; + + pub const Event = union(EventType) { + rename: EventPathString, + change: EventPathString, + @"error": bun.sys.Error, + abort: void, + close: void, + + pub fn dupe(event: Event) !Event { + return switch (event) { + inline .rename, .change => |path, t| @unionInit(Event, @tagName(t), try bun.default_allocator.dupe(u8, path)), + inline else => |value, t| @unionInit(Event, @tagName(t), value), + }; + } + + pub fn deinit(event: *Event) void { + switch (event.*) { + .rename, .change => |*path| switch (Environment.os) { + else => bun.default_allocator.free(path.*), + .windows => path.deinit(), + }, + else => {}, + } + } + }; + pub const EventType = enum(u8) { rename = 0, change = 1, @@ -171,16 +196,15 @@ pub const FSWatcher = struct { }; pub const FSWatchTaskWindows = struct { - file_path: StringOrBytesToDecode = .{ .bytes_to_free = "" }, - event_type: EventType = .@"error", - ctx: *FSWatcher = undefined, + event: Event = .{ .@"error" = .{ .errno = @intFromEnum(bun.C.SystemErrno.EINVAL), .syscall = .watch } }, + ctx: *FSWatcher, - // To match the API of the posix version + /// Unused: To match the API of the posix version count: u0 = 0, pub usingnamespace bun.New(@This()); - const StringOrBytesToDecode = union(enum) { + pub const StringOrBytesToDecode = union(enum) { string: bun.String, bytes_to_free: []const u8, @@ -193,40 +217,42 @@ pub const FSWatcher = struct { }, } } + + pub fn format(this: *const StringOrBytesToDecode, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + switch (this.*) { + .string => |str| try writer.print("{}", .{str}), + .bytes_to_free => |utf8| try writer.print("{s}", .{utf8}), + } + } }; pub fn appendAbort(this: *FSWatchTaskWindows) void { const ctx = this.ctx; const task = FSWatchTaskWindows.new(.{ .ctx = ctx, - .file_path = .{ .bytes_to_free = "" }, - .event_type = .abort, + .event = .abort, }); ctx.eventLoop().enqueueTask(JSC.Task.init(task)); } + /// this runs on JS Context Thread pub fn run(this: *FSWatchTaskWindows) void { - // this runs on JS Context Thread var ctx = this.ctx; - switch (this.event_type) { - inline .rename, .change => |eventType| { + switch (this.event) { + inline .rename, .change => |*path, event_type| { if (ctx.encoding == .utf8) { - ctx.emitWithFilename(this.file_path.string.transferToJS(ctx.globalThis), eventType); + ctx.emitWithFilename(path.string.transferToJS(ctx.globalThis), event_type); } else { - const bytes = this.file_path.bytes_to_free; - this.file_path.bytes_to_free = ""; - ctx.emit(bytes, eventType); + const bytes = path.bytes_to_free; + path.bytes_to_free = ""; + ctx.emit(bytes, event_type); bun.default_allocator.free(bytes); } }, - .@"error" => { - // file_path is the error message in this case - const err = this.file_path.bytes_to_free; - this.file_path.bytes_to_free = ""; + .@"error" => |err| { ctx.emitError(err); - bun.default_allocator.free(err); }, .abort => { ctx.emitIfAborted(); @@ -240,45 +266,44 @@ pub const FSWatcher = struct { } pub fn deinit(this: *FSWatchTaskWindows) void { - this.file_path.deinit(); + this.event.deinit(); this.destroy(); } }; - pub fn onPathUpdatePosix(ctx: ?*anyopaque, path: string, is_file: bool, event_type: PathWatcher.PathWatcher.EventType) void { + pub fn onPathUpdatePosix(ctx: ?*anyopaque, event: Event, is_file: bool) void { const this = bun.cast(*FSWatcher, ctx.?); - const relative_path = bun.default_allocator.dupe(u8, path) catch unreachable; - - if (this.verbose and event_type != .@"error") { - if (is_file) { - Output.prettyErrorln(" File changed: {s}", .{relative_path}); - } else { - Output.prettyErrorln(" Dir changed: {s}", .{relative_path}); + if (this.verbose) { + switch (event) { + .rename, .change => |value| { + if (is_file) { + Output.prettyErrorln(" File changed: {s}", .{value}); + } else { + Output.prettyErrorln(" Dir changed: {s}", .{value}); + } + }, + else => {}, } } - switch (event_type) { - .rename => { - this.current_task.append(relative_path, .rename, true); - }, - .change => { - this.current_task.append(relative_path, .change, true); - }, - else => { - this.current_task.append(relative_path, .@"error", true); - }, - } + const cloned = event.dupe() catch bun.outOfMemory(); + this.current_task.append(cloned, true); } - pub fn onPathUpdateWindows(ctx: ?*anyopaque, relative_path: string, is_file: bool, event_type: PathWatcher.PathWatcher.EventType) void { + pub fn onPathUpdateWindows(ctx: ?*anyopaque, event: Event, is_file: bool) void { const this = bun.cast(*FSWatcher, ctx.?); - if (this.verbose and event_type != .@"error") { - if (is_file) { - Output.prettyErrorln(" File changed: {s}", .{relative_path}); - } else { - Output.prettyErrorln(" Dir changed: {s}", .{relative_path}); + if (this.verbose) { + switch (event) { + .rename, .change => |value| { + if (is_file) { + Output.prettyErrorln(" File changed: {}", .{value}); + } else { + Output.prettyErrorln(" Dir changed: {}", .{value}); + } + }, + else => {}, } } @@ -286,30 +311,11 @@ pub const FSWatcher = struct { return; } - const encoding = this.encoding; - switch (event_type) { - inline .rename, .change => |eventType| { - const task = FSWatchTaskWindows.new(.{ - .ctx = this, - .file_path = switch (encoding) { - .utf8 => .{ .string = bun.String.createUTF8(relative_path) }, - else => .{ .bytes_to_free = bun.default_allocator.dupeZ(u8, relative_path) catch bun.outOfMemory() }, - }, - .event_type = eventType, - }); - - this.eventLoop().enqueueTask(JSC.Task.init(task)); - }, - else => { - const task = FSWatchTaskWindows.new(.{ - .ctx = this, - .file_path = .{ .bytes_to_free = bun.default_allocator.dupeZ(u8, relative_path) catch bun.outOfMemory() }, - .event_type = .@"error", - }); - - this.eventLoop().enqueueTask(JSC.Task.init(task)); - }, - } + const task = FSWatchTaskWindows.new(.{ + .ctx = this, + .event = event, + }); + this.eventLoop().enqueueTask(JSC.Task.init(task)); } pub const onPathUpdate = if (Environment.isWindows) onPathUpdateWindows else onPathUpdatePosix; @@ -472,12 +478,11 @@ pub const FSWatcher = struct { }; } - pub fn createFSWatcher(this: Arguments) !JSC.JSValue { - const obj = try FSWatcher.init(this); - if (obj.js_this != .zero) { - return obj.js_this; - } - return JSC.JSValue.jsUndefined(); + pub fn createFSWatcher(this: Arguments) JSC.Maybe(JSC.JSValue) { + return switch (FSWatcher.init(this)) { + .result => |result| .{ .result = result.js_this }, + .err => |err| .{ .err = err }, + }; } }; @@ -539,7 +544,7 @@ pub const FSWatcher = struct { } } } - pub fn emitError(this: *FSWatcher, err: string) void { + pub fn emitError(this: *FSWatcher, err: bun.sys.Error) void { if (this.closed) return; defer this.close(); @@ -551,7 +556,7 @@ pub const FSWatcher = struct { const globalObject = this.globalThis; var args = [_]JSC.JSValue{ EventType.@"error".toJS(globalObject), - JSC.ZigString.fromUTF8(err).toErrorInstance(globalObject), + err.toJSC(globalObject), }; _ = listener.callWithGlobalThis( globalObject, @@ -568,7 +573,8 @@ pub const FSWatcher = struct { emitJS(listener, this.globalThis, file_name, eventType); } - pub fn emit(this: *FSWatcher, file_name: string, comptime eventType: EventType) void { + pub fn emit(this: *FSWatcher, file_name: string, comptime event_type: EventType) void { + bun.assert(event_type != .@"error"); const js_this = this.js_this; if (js_this == .zero) return; const listener = FSWatcher.listenerGetCached(js_this) orelse return; @@ -585,12 +591,12 @@ pub const FSWatcher = struct { } } - emitJS(listener, globalObject, filename, eventType); + emitJS(listener, globalObject, filename, event_type); } - fn emitJS(listener: JSC.JSValue, globalObject: *JSC.JSGlobalObject, filename: JSC.JSValue, comptime eventType: EventType) void { + fn emitJS(listener: JSC.JSValue, globalObject: *JSC.JSGlobalObject, filename: JSC.JSValue, comptime event_type: EventType) void { var args = [_]JSC.JSValue{ - eventType.toJS(globalObject), + event_type.toJS(globalObject), filename, }; @@ -650,9 +656,7 @@ pub const FSWatcher = struct { _ = this.pending_activity_count.fetchSub(1, .Monotonic); } - pub fn close( - this: *FSWatcher, - ) void { + pub fn close(this: *FSWatcher) void { this.mutex.lock(); if (!this.closed) { this.closed = true; @@ -704,8 +708,8 @@ pub const FSWatcher = struct { this.deinit(); } - pub fn init(args: Arguments) !*FSWatcher { - var buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + pub fn init(args: Arguments) bun.JSC.Maybe(*FSWatcher) { + var buf: bun.PathBuffer = undefined; var slice = args.path.slice(); if (bun.strings.startsWith(slice, "file://")) { slice = slice[6..]; @@ -715,7 +719,10 @@ pub const FSWatcher = struct { slice, }; - const cwd = try bun.getcwd(&buf); + const cwd = switch (bun.sys.getcwd(&buf)) { + .result => |r| r, + .err => |err| return .{ .err = err }, + }; buf[cwd.len] = std.fs.path.sep; var joined_buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; @@ -749,13 +756,17 @@ pub const FSWatcher = struct { }); ctx.current_task.ctx = ctx; - errdefer ctx.deinit(); - ctx.path_watcher = if (args.signal == null or !args.signal.?.aborted()) - try PathWatcher.watch(vm, file_path_z, args.recursive, onPathUpdate, onUpdateEnd, bun.cast(*anyopaque, ctx)) + switch (PathWatcher.watch(vm, file_path_z, args.recursive, onPathUpdate, onUpdateEnd, bun.cast(*anyopaque, ctx))) { + .result => |r| r, + .err => |err| { + ctx.deinit(); + return .{ .err = err }; + }, + } else null; ctx.initJS(args.listener); - return ctx; + return .{ .result = ctx }; } }; diff --git a/src/bun.js/node/path_watcher.zig b/src/bun.js/node/path_watcher.zig index 17394291490e97..7b09dc0f4f76b3 100644 --- a/src/bun.js/node/path_watcher.zig +++ b/src/bun.js/node/path_watcher.zig @@ -21,6 +21,10 @@ const Semaphore = sync.Semaphore; var default_manager_mutex: Mutex = Mutex.init(); var default_manager: ?*PathWatcherManager = null; +const FSWatcher = bun.JSC.Node.FSWatcher; +const Event = FSWatcher.Event; +const StringOrBytesToDecode = FSWatcher.FSWatchTaskWindows.StringOrBytesToDecode; + pub const PathWatcherManager = struct { const options = @import("../../options.zig"); pub const Watcher = GenericWatcher.NewWatcher(*PathWatcherManager); @@ -71,66 +75,74 @@ pub const PathWatcherManager = struct { this.deinit(); } } - // TODO: switch to using JSC.Maybe to avoid using "unreachable" and improve error messages + fn _fdFromAbsolutePathZ( this: *PathWatcherManager, path: [:0]const u8, - ) !PathInfo { + ) bun.JSC.Maybe(PathInfo) { this.mutex.lock(); defer this.mutex.unlock(); if (this.file_paths.getEntry(path)) |entry| { var info = entry.value_ptr; info.refs += 1; - return info.*; - } - const cloned_path = try bun.default_allocator.dupeZ(u8, path); - errdefer bun.default_allocator.free(cloned_path); - - if (std.fs.openDirAbsoluteZ(cloned_path, .{ - .access_sub_paths = true, - .iterate = true, - })) |iterable_dir| { - const result = PathInfo{ - .fd = bun.toFD(iterable_dir.fd), - .is_file = false, - .path = cloned_path, - .dirname = cloned_path, - .hash = GenericWatcher.getHash(cloned_path), - .refs = 1, - }; - _ = try this.file_paths.put(cloned_path, result); - return result; - } else |err| { - if (err == error.NotDir) { - const file = try std.fs.openFileAbsoluteZ(cloned_path, .{ .mode = .read_only }); + return .{ .result = info.* }; + } + + switch (switch (Environment.os) { + else => bun.sys.open(path, std.os.O.DIRECTORY | std.os.O.RDONLY, 0), + // windows bun.sys.open does not pass iterable=true, + .windows => bun.sys.openDirAtWindowsA(bun.toFD(std.fs.cwd().fd), path, .{ .iterable = true, .read_only = true }), + }) { + .err => |e| { + if (e.errno == @intFromEnum(bun.C.E.NOTDIR)) { + const file = switch (bun.sys.open(path, 0, 0)) { + .err => |file_err| return .{ .err = file_err.withPath(path) }, + .result => |r| r, + }; + const cloned_path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(); + const result = PathInfo{ + .fd = file, + .is_file = true, + .path = cloned_path, + // if is really a file we need to get the dirname + .dirname = std.fs.path.dirname(cloned_path) orelse cloned_path, + .hash = GenericWatcher.getHash(cloned_path), + .refs = 1, + }; + _ = this.file_paths.put(cloned_path, result) catch bun.outOfMemory(); + return .{ .result = result }; + } + return .{ .err = e.withPath(path) }; + }, + .result => |iterable_dir| { + const cloned_path = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(); const result = PathInfo{ - .fd = bun.toFD(file.handle), - .is_file = true, + .fd = iterable_dir, + .is_file = false, .path = cloned_path, - // if is really a file we need to get the dirname - .dirname = std.fs.path.dirname(cloned_path) orelse cloned_path, + .dirname = cloned_path, .hash = GenericWatcher.getHash(cloned_path), .refs = 1, }; - _ = try this.file_paths.put(cloned_path, result); - return result; - } else { - return err; - } + _ = this.file_paths.put(cloned_path, result) catch bun.outOfMemory(); + return .{ .result = result }; + }, } - - unreachable; } - pub fn init(vm: *JSC.VirtualMachine) !*PathWatcherManager { - const this = try bun.default_allocator.create(PathWatcherManager); + const PathWatcherManagerError = std.mem.Allocator.Error || + std.os.KQueueError || + error{KQueueError} || + std.os.INotifyInitError || + std.Thread.SpawnError; + + pub fn init(vm: *JSC.VirtualMachine) PathWatcherManagerError!*PathWatcherManager { + const this = bun.default_allocator.create(PathWatcherManager) catch bun.outOfMemory(); errdefer bun.default_allocator.destroy(this); - var watchers = bun.BabyList(?*PathWatcher).initCapacity(bun.default_allocator, 1) catch |err| { - bun.default_allocator.destroy(this); - return err; - }; + var watchers = bun.BabyList(?*PathWatcher).initCapacity(bun.default_allocator, 1) catch bun.outOfMemory(); errdefer watchers.deinitWithAllocator(bun.default_allocator); + const manager = PathWatcherManager{ .file_paths = bun.StringHashMap(PathInfo).init(bun.default_allocator), .current_fd_task = bun.FDHashMap(*DirectoryRegisterTask).init(bun.default_allocator), @@ -246,7 +258,7 @@ pub const PathWatcherManager = struct { if (path.len == 0 or (bun.strings.containsChar(path, '/') and !watcher.recursive)) { continue; } - watcher.emit(path, hash, timestamp, true, event_type); + watcher.emit(event_type.toEvent(path), hash, timestamp, true); } } } @@ -298,7 +310,7 @@ pub const PathWatcherManager = struct { continue; } - watcher.emit(path, hash, timestamp, false, event_type); + watcher.emit(event_type.toEvent(path), hash, timestamp, false); } } } @@ -318,7 +330,7 @@ pub const PathWatcherManager = struct { pub fn onError( this: *PathWatcherManager, - err: anyerror, + err: bun.sys.Error, ) void { { this.mutex.lock(); @@ -329,8 +341,8 @@ pub const PathWatcherManager = struct { // stop all watchers for (watchers) |w| { if (w) |watcher| { - log("[watch] error: {s}", .{@errorName(err)}); - watcher.emit(@errorName(err), 0, timestamp, false, .@"error"); + log("[watch] error: {}", .{err}); + watcher.emit(.{ .@"error" = err }, 0, timestamp, false); watcher.flush(); } } @@ -340,6 +352,7 @@ pub const PathWatcherManager = struct { defer default_manager_mutex.unlock(); default_manager = null; } + // deinit manager when all watchers are closed this.deinit(); } @@ -428,18 +441,27 @@ pub const PathWatcherManager = struct { this: *DirectoryRegisterTask, watcher: *PathWatcher, buf: *[bun.MAX_PATH_BYTES + 1]u8, - ) !void { - if (comptime Environment.isWindows) { - bun.todo(@src(), "implement directory watching on windows"); - return; - } + ) bun.JSC.Maybe(void) { + if (Environment.isWindows) @compileError("use win_watcher.zig"); + const manager = this.manager; const path = this.path; const fd = path.fd; var iter = fd.asDir().iterate(); // now we iterate over all files and directories - while (try iter.next()) |entry| { + while (iter.next() catch |err| { + return .{ + .err = .{ + .errno = @truncate(@intFromEnum(switch (err) { + error.AccessDenied => bun.C.E.ACCES, + error.SystemResources => bun.C.E.NOMEM, + error.Unexpected => bun.C.E.INVAL, + })), + .syscall = .watch, + }, + }; + }) |entry| { var parts = [2]string{ path.path, entry.name }; const entry_path = Path.joinAbsStringBuf( Fs.FileSystem.instance.topLevelDirWithoutTrailingSlash(), @@ -451,26 +473,50 @@ pub const PathWatcherManager = struct { buf[entry_path.len] = 0; const entry_path_z = buf[0..entry_path.len :0]; - const child_path = try manager._fdFromAbsolutePathZ(entry_path_z); + const child_path = switch (manager._fdFromAbsolutePathZ(entry_path_z)) { + .result => |result| result, + .err => |e| return .{ .err = e }, + }; + { watcher.mutex.lock(); defer watcher.mutex.unlock(); watcher.file_paths.push(bun.default_allocator, child_path.path) catch |err| { manager._decrementPathRef(entry_path_z); - return err; + return switch (err) { + error.OutOfMemory => .{ .err = .{ + .errno = @truncate(@intFromEnum(bun.C.E.NOMEM)), + .syscall = .watch, + } }, + }; }; } // we need to call this unlocked if (child_path.is_file) { - try manager.main_watcher.addFile(child_path.fd, child_path.path, child_path.hash, options.Loader.file, .zero, null, false); + switch (manager.main_watcher.addFile( + child_path.fd, + child_path.path, + child_path.hash, + options.Loader.file, + .zero, + null, + false, + )) { + .err => |err| return .{ .err = err }, + .result => {}, + } } else { if (watcher.recursive and !watcher.isClosed()) { // this may trigger another thread with is desired when available to watch long trees - try manager._addDirectory(watcher, child_path); + switch (manager._addDirectory(watcher, child_path)) { + .err => |err| return .{ .err = err }, + .result => {}, + } } } } + return .{ .result = {} }; } fn run(this: *DirectoryRegisterTask) void { @@ -482,11 +528,14 @@ pub const PathWatcherManager = struct { while (this.getNext()) |watcher| { defer watcher.unrefPendingDirectory(); - this.processWatcher(watcher, &buf) catch |err| { - log("[watch] error registering directory: {s}", .{@errorName(err)}); - watcher.emit(@errorName(err), 0, std.time.milliTimestamp(), false, .@"error"); - watcher.flush(); - }; + switch (this.processWatcher(watcher, &buf)) { + .err => |err| { + log("[watch] error registering directory: {s}", .{err}); + watcher.emit(.{ .@"error" = err }, 0, std.time.milliTimestamp(), false); + watcher.flush(); + }, + .result => {}, + } } this.manager.unrefPendingTask(); @@ -498,11 +547,23 @@ pub const PathWatcherManager = struct { }; // this should only be called if thread pool is not null - fn _addDirectory(this: *PathWatcherManager, watcher: *PathWatcher, path: PathInfo) !void { + fn _addDirectory(this: *PathWatcherManager, watcher: *PathWatcher, path: PathInfo) bun.JSC.Maybe(void) { const fd = path.fd; - try this.main_watcher.addDirectory(fd, path.path, path.hash, false); + switch (this.main_watcher.addDirectory(fd, path.path, path.hash, false)) { + .err => |err| return .{ .err = err }, + .result => {}, + } - return try DirectoryRegisterTask.schedule(this, watcher, path); + return .{ + .result = DirectoryRegisterTask.schedule(this, watcher, path) catch |err| return .{ + .err = .{ + .errno = @truncate(@intFromEnum(switch (err) { + error.OutOfMemory => bun.C.E.NOMEM, + error.UnexpectedFailure => bun.C.E.INVAL, + })), + }, + }, + }; } // register is always called form main thread @@ -531,14 +592,14 @@ pub const PathWatcherManager = struct { const path = watcher.path; if (path.is_file) { - try this.main_watcher.addFile(path.fd, path.path, path.hash, options.Loader.file, .zero, null, false); + try this.main_watcher.addFile(path.fd, path.path, path.hash, options.Loader.file, .zero, null, false).unwrap(); } else { if (comptime Environment.isMac) { if (watcher.fsevents_watcher != null) { return; } } - try this._addDirectory(watcher, path); + try this._addDirectory(watcher, path).unwrap(); } } @@ -696,9 +757,15 @@ pub const PathWatcher = struct { pub const EventType = enum { rename, change, - @"error", + + pub fn toEvent(event_type: EventType, path: FSWatcher.EventPathString) Event { + return switch (event_type) { + inline else => |t| @unionInit(Event, @tagName(t), path), + }; + } }; - const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, event_type: EventType) void; + + pub const Callback = *const fn (ctx: ?*anyopaque, detail: Event, is_file: bool) void; const UpdateEndCallback = *const fn (ctx: ?*anyopaque) void; pub fn init(manager: *PathWatcherManager, path: PathWatcherManager.PathInfo, recursive: bool, callback: Callback, updateEndCallback: UpdateEndCallback, ctx: ?*anyopaque) !*PathWatcher { @@ -722,8 +789,8 @@ pub const PathWatcher = struct { .fsevents_watcher = FSEvents.watch( resolved_path, recursive, - bun.cast(FSEvents.FSEventsWatcher.Callback, callback), - bun.cast(FSEvents.FSEventsWatcher.UpdateEndCallback, updateEndCallback), + callback, + updateEndCallback, bun.cast(*anyopaque, ctx), ) catch |err| { bun.default_allocator.destroy(this); @@ -805,17 +872,32 @@ pub const PathWatcher = struct { } } - pub fn emit(this: *PathWatcher, path: string, hash: GenericWatcher.HashType, time_stamp: i64, is_file: bool, event_type: EventType) void { - const time_diff = time_stamp - this.last_change_event.time_stamp; - // skip consecutive duplicates - if ((this.last_change_event.time_stamp == 0 or time_diff > 1) or this.last_change_event.event_type != event_type and this.last_change_event.hash != hash) { - this.last_change_event.time_stamp = time_stamp; - this.last_change_event.event_type = event_type; - this.last_change_event.hash = hash; - this.needs_flush = true; - if (this.isClosed()) return; - this.callback(this.ctx, path, is_file, event_type); + pub fn emit(this: *PathWatcher, event: Event, hash: GenericWatcher.HashType, time_stamp: i64, is_file: bool) void { + switch (event) { + .change, .rename => { + const event_type = switch (event) { + inline .change, .rename => |_, t| @field(EventType, @tagName(t)), + else => unreachable, // above switch guarentees this subset + }; + + const time_diff = time_stamp - this.last_change_event.time_stamp; + if (!((this.last_change_event.time_stamp == 0 or time_diff > 1) or + this.last_change_event.event_type != event_type and + this.last_change_event.hash != hash)) + { + // skip consecutive duplicates + return; + } + + this.last_change_event.time_stamp = time_stamp; + this.last_change_event.event_type = event_type; + }, + else => {}, } + + this.needs_flush = true; + if (this.isClosed()) return; + this.callback(this.ctx, event, is_file); } pub fn flush(this: *PathWatcher) void { @@ -868,20 +950,76 @@ pub fn watch( comptime callback: PathWatcher.Callback, comptime updateEnd: PathWatcher.UpdateEndCallback, ctx: ?*anyopaque, -) !*PathWatcher { - if (default_manager) |manager| { - const path_info = try manager._fdFromAbsolutePathZ(path); - errdefer manager._decrementPathRef(path); - return try PathWatcher.init(manager, path_info, recursive, callback, updateEnd, ctx); - } else { +) bun.JSC.Maybe(*PathWatcher) { + const manager = default_manager orelse brk: { default_manager_mutex.lock(); defer default_manager_mutex.unlock(); if (default_manager == null) { - default_manager = try PathWatcherManager.init(vm); + default_manager = PathWatcherManager.init(vm) catch |e| { + return .{ .err = .{ + .errno = @truncate(@intFromEnum(switch (e) { + error.SystemResources, error.LockedMemoryLimitExceeded, error.OutOfMemory => bun.C.E.NOMEM, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.ThreadQuotaExceeded, + => bun.C.E.MFILE, + + error.Unexpected => bun.C.E.NOMEM, + + error.KQueueError => bun.C.E.INVAL, + })), + .syscall = .watch, + } }; + }; } - const manager = default_manager.?; - const path_info = try manager._fdFromAbsolutePathZ(path); - errdefer manager._decrementPathRef(path); - return try PathWatcher.init(manager, path_info, recursive, callback, updateEnd, ctx); - } + break :brk default_manager.?; + }; + + const path_info = switch (manager._fdFromAbsolutePathZ(path)) { + .result => |result| result, + .err => |err| return .{ .err = err }, + }; + + const watcher = PathWatcher.init(manager, path_info, recursive, callback, updateEnd, ctx) catch |e| { + bun.handleErrorReturnTrace(e, @errorReturnTrace()); + manager._decrementPathRef(path); + + return .{ .err = .{ + .errno = @truncate(@intFromEnum(switch (e) { + error.Unexpected, + error.UnexpectedFailure, + error.WatchAlreadyExists, + error.NameTooLong, + error.BadPathName, + error.InvalidUtf8, + => bun.C.E.INVAL, + + error.OutOfMemory, + error.SystemResources, + => bun.C.E.NOMEM, + + error.FileNotFound, + error.NetworkNotFound, + error.NoDevice, + => bun.C.E.NOENT, + + error.DeviceBusy => bun.C.E.BUSY, + error.AccessDenied => bun.C.E.PERM, + error.InvalidHandle => bun.C.E.BADF, + error.SymLinkLoop => bun.C.E.LOOP, + error.NotDir => bun.C.E.NOTDIR, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.UserResourceLimitReached, + => bun.C.E.MFILE, + + else => bun.C.E.INVAL, + })), + .syscall = .watch, + } }; + }; + + return .{ .result = watcher }; } diff --git a/src/bun.js/node/win_watcher.zig b/src/bun.js/node/win_watcher.zig index a7798b09b1093a..9d3b806b0732f0 100644 --- a/src/bun.js/node/win_watcher.zig +++ b/src/bun.js/node/win_watcher.zig @@ -12,6 +12,10 @@ const StoredFileDescriptorType = bun.StoredFileDescriptorType; const Output = bun.Output; const Watcher = @import("../../watcher.zig"); +const FSWatcher = bun.JSC.Node.FSWatcher; +const EventType = @import("./path_watcher.zig").PathWatcher.EventType; +const Event = FSWatcher.Event; + var default_manager: ?*PathWatcherManager = null; // TODO: make this a generic so we can reuse code with path_watcher @@ -26,7 +30,7 @@ pub const PathWatcherManager = struct { pub usingnamespace bun.New(PathWatcherManager); - pub fn init(vm: *JSC.VirtualMachine) !*PathWatcherManager { + pub fn init(vm: *JSC.VirtualMachine) *PathWatcherManager { return PathWatcherManager.new(.{ .watchers = .{}, .vm = vm, @@ -107,11 +111,10 @@ pub const PathWatcher = struct { } }; - pub const EventType = JSC.Node.FSWatcher.EventType; - const Callback = *const fn (ctx: ?*anyopaque, path: string, is_file: bool, event_type: EventType) void; + const Callback = *const fn (ctx: ?*anyopaque, event: Event, is_file: bool) void; const UpdateEndCallback = *const fn (ctx: ?*anyopaque) void; - fn uvEventCallback(event: *uv.uv_fs_event_t, filename: ?[*:0]const u8, events: c_int, status: c_int) callconv(.C) void { + fn uvEventCallback(event: *uv.uv_fs_event_t, filename: ?[*:0]const u8, events: c_int, status: uv.ReturnCode) callconv(.C) void { if (event.data == null) { Output.debugWarn("uvEventCallback called with null data", .{}); return; @@ -123,17 +126,14 @@ pub const PathWatcher = struct { const timestamp = event.loop.time; - if (status < 0) { - const err_name = uv.uv_err_name(status); - const err = err_name[0..bun.len(err_name)]; - { - this.emit_in_progress = true; - const ctxs = this.handlers.keys(); - for (ctxs) |ctx| { - onPathUpdateFn(ctx, err, false, .@"error"); - onUpdateEndFn(ctx); - } - this.emit_in_progress = false; + if (status.toError(.watch)) |err| { + this.emit_in_progress = true; + defer this.emit_in_progress = false; + + const ctxs = this.handlers.keys(); + for (ctxs) |ctx| { + onPathUpdateFn(ctx, .{ .@"error" = err }, false); + onUpdateEndFn(ctx); } this.maybeDeinit(); @@ -142,46 +142,62 @@ pub const PathWatcher = struct { const path = if (filename) |file| file[0..bun.len(file) :0] else return; - this.emit(path, @truncate(event.hash(path, events, status)), timestamp, !event.isDir(), if (events & uv.UV_RENAME != 0) .rename else .change); + this.emit( + path, + @truncate(event.hash(path, events, status)), + timestamp, + !event.isDir(), + if (events & uv.UV_RENAME != 0) .rename else .change, + ); } pub fn emit(this: *PathWatcher, path: string, hash: Watcher.HashType, timestamp: u64, is_file: bool, event_type: EventType) void { this.emit_in_progress = true; - var debug_count: if (bun.Environment.isDebug) usize else u0 = if (comptime bun.Environment.isDebug) 0 else 0; + var debug_count: if (bun.Environment.isDebug) usize else u0 = 0; for (this.handlers.values(), 0..) |*event, i| { if (event.emit(hash, timestamp, event_type)) { - const ctx = this.handlers.keys()[i]; - onPathUpdateFn(ctx, path, is_file, event_type); + const ctx: *FSWatcher = @alignCast(@ptrCast(this.handlers.keys()[i])); + onPathUpdateFn(ctx, event_type.toEvent(switch (ctx.encoding) { + .utf8 => .{ .string = bun.String.createUTF8(path) }, + else => .{ .bytes_to_free = bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory() }, + }), is_file); if (comptime bun.Environment.isDebug) debug_count += 1; onUpdateEndFn(ctx); } } if (comptime bun.Environment.isDebug) - log("emit({s}, {s}, {s}, at {d}) x {d}", .{ path, if (is_file) "file" else "dir", @tagName(event_type), timestamp, debug_count }); + log("emit({s}, {s}, {s}, at {d}) x {d}", .{ + path, + if (is_file) "file" else "dir", + @tagName(event_type), + timestamp, + debug_count, + }); this.emit_in_progress = false; this.maybeDeinit(); } - pub fn init(manager: *PathWatcherManager, path: [:0]const u8, recursive: bool) !*PathWatcher { + pub fn init(manager: *PathWatcherManager, path: [:0]const u8, recursive: bool) bun.JSC.Maybe(*PathWatcher) { var outbuf: [bun.MAX_PATH_BYTES]u8 = undefined; - const event_path = brk: { - const size = bun.sys.readlink(path, &outbuf).unwrap() catch |err| { - if (err == error.ENOENT) { - return error.ENOENT; + const event_path = switch (bun.sys.readlink(path, &outbuf)) { + .err => |err| brk: { + if (err.errno == @intFromEnum(bun.C.E.NOENT)) { + return .{ .err = .{ + .errno = err.errno, + .syscall = .open, + } }; } break :brk path; - }; - if (size >= bun.MAX_PATH_BYTES) break :brk path; - outbuf[size] = 0; - break :brk outbuf[0..size]; + }, + .result => |event_path| event_path, }; - const watchers_entry = try manager.watchers.getOrPut(bun.default_allocator, @as([]const u8, event_path)); + const watchers_entry = manager.watchers.getOrPut(bun.default_allocator, @as([]const u8, event_path)) catch bun.outOfMemory(); if (watchers_entry.found_existing) { - return watchers_entry.value_ptr.*; + return .{ .result = watchers_entry.value_ptr.* }; } var this = PathWatcher.new(.{ @@ -195,22 +211,27 @@ pub const PathWatcher = struct { this.deinit(); } - if (uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle) != 0) { - return error.FailedToInitializeFSEvent; + if (uv.uv_fs_event_init(manager.vm.uvLoop(), &this.handle).toError(.watch)) |err| { + return .{ .err = err }; } this.handle.data = this; // UV_FS_EVENT_RECURSIVE only works for Windows and OSX - if (uv.uv_fs_event_start(&this.handle, PathWatcher.uvEventCallback, event_path.ptr, if (recursive) uv.UV_FS_EVENT_RECURSIVE else 0) != 0) { - return error.FailedToStartFSEvent; + if (uv.uv_fs_event_start( + &this.handle, + PathWatcher.uvEventCallback, + event_path.ptr, + if (recursive) uv.UV_FS_EVENT_RECURSIVE else 0, + ).toError(.watch)) |err| { + return .{ .err = err }; } // we handle this in node_fs_watcher uv.uv_unref(@ptrCast(&this.handle)); watchers_entry.value_ptr.* = this; - watchers_entry.key_ptr.* = try bun.default_allocator.dupeZ(u8, event_path); + watchers_entry.key_ptr.* = bun.default_allocator.dupeZ(u8, event_path) catch bun.outOfMemory(); - return this; + return .{ .result = this }; } fn uvClosedCallback(handler: *anyopaque) callconv(.C) void { @@ -260,7 +281,7 @@ pub fn watch( comptime callback: PathWatcher.Callback, comptime updateEnd: PathWatcher.UpdateEndCallback, ctx: *anyopaque, -) !*PathWatcher { +) bun.JSC.Maybe(*PathWatcher) { comptime { if (callback != onPathUpdateFn) { @compileError("callback must be onPathUpdateFn"); @@ -272,14 +293,17 @@ pub fn watch( } if (!bun.Environment.isWindows) { - @panic("win_watcher should only be used on Windows"); + @compileError("win_watcher should only be used on Windows"); } const manager = default_manager orelse brk: { - default_manager = try PathWatcherManager.init(vm); + default_manager = PathWatcherManager.init(vm); break :brk default_manager.?; }; - var watcher = try PathWatcher.init(manager, path, recursive); - try watcher.handlers.put(bun.default_allocator, ctx, .{}); - return watcher; + var watcher = switch (PathWatcher.init(manager, path, recursive)) { + .err => |err| return .{ .err = err }, + .result => |watcher| watcher, + }; + watcher.handlers.put(bun.default_allocator, ctx, .{}) catch bun.outOfMemory(); + return .{ .result = watcher }; } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 22476a5ba45891..c5d9daa1323890 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -2109,7 +2109,7 @@ pub const BundleV2 = struct { if (this.bun_watcher != null) { if (empty_result.watcher_data.fd != .zero and empty_result.watcher_data.fd != bun.invalid_fd) { - this.bun_watcher.?.addFile( + _ = this.bun_watcher.?.addFile( empty_result.watcher_data.fd, input_files.items(.source)[empty_result.source_index.get()].path.text, bun.hash32(input_files.items(.source)[empty_result.source_index.get()].path.text), @@ -2117,7 +2117,7 @@ pub const BundleV2 = struct { empty_result.watcher_data.dir_fd, null, false, - ) catch {}; + ); } } }, @@ -2128,7 +2128,7 @@ pub const BundleV2 = struct { // to minimize contention, we add watcher here if (this.bun_watcher != null) { if (result.watcher_data.fd != .zero and result.watcher_data.fd != bun.invalid_fd) { - this.bun_watcher.?.addFile( + _ = this.bun_watcher.?.addFile( result.watcher_data.fd, result.source.path.text, bun.hash32(result.source.path.text), @@ -2136,7 +2136,7 @@ pub const BundleV2 = struct { result.watcher_data.dir_fd, result.watcher_data.package_json, false, - ) catch {}; + ); } } } diff --git a/src/cli.zig b/src/cli.zig index f21a6bbf8f753e..c2531b476f919b 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -418,12 +418,12 @@ pub const Arguments = struct { } var cwd: []u8 = undefined; - if (args.option("--cwd")) |cwd_| { + if (args.option("--cwd")) |cwd_arg| { cwd = brk: { var outbuf: [bun.MAX_PATH_BYTES]u8 = undefined; - const out = bun.path.joinAbs(try bun.getcwd(&outbuf), .loose, cwd_); + const out = bun.path.joinAbs(try bun.getcwd(&outbuf), .loose, cwd_arg); bun.sys.chdir(out).unwrap() catch |err| { - Output.prettyErrorln("{}\n", .{err}); + Output.err(err, "Could not change directory to \"{s}\"\n", .{cwd_arg}); Global.exit(1); }; break :brk try allocator.dupe(u8, out); diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index b6c8a16bd94772..8c737764a78f86 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -1522,7 +1522,7 @@ pub const struct_uv_fs_event_req_s = extern struct { next_req: [*c]struct_uv_req_s, }; pub const uv_fs_event_t = struct_uv_fs_event_s; -pub const uv_fs_event_cb = ?*const fn (*uv_fs_event_t, [*c]const u8, c_int, c_int) callconv(.C) void; +pub const uv_fs_event_cb = ?*const fn (*uv_fs_event_t, [*c]const u8, c_int, ReturnCode) callconv(.C) void; pub const struct_uv_fs_event_s = extern struct { data: ?*anyopaque, loop: *uv_loop_t, @@ -1546,7 +1546,7 @@ pub const struct_uv_fs_event_s = extern struct { return this.dirw != null; } - pub fn hash(this: *const uv_fs_event_t, filename: []const u8, events: c_int, status: c_int) u64 { + pub fn hash(this: *const uv_fs_event_t, filename: []const u8, events: c_int, status: ReturnCode) u64 { var hasher = std.hash.Wyhash.init(0); if (this.path) |path| { hasher.update(bun.sliceTo(path, 0)); @@ -2437,10 +2437,11 @@ pub const UV_FS_EVENT_WATCH_ENTRY: c_int = 1; pub const UV_FS_EVENT_STAT: c_int = 2; pub const UV_FS_EVENT_RECURSIVE: c_int = 4; pub const enum_uv_fs_event_flags = c_uint; -pub extern fn uv_fs_event_init(loop: *uv_loop_t, handle: *uv_fs_event_t) c_int; -pub extern fn uv_fs_event_start(handle: *uv_fs_event_t, cb: uv_fs_event_cb, path: [*]const u8, flags: c_uint) c_int; +pub extern fn uv_fs_event_init(loop: *uv_loop_t, handle: *uv_fs_event_t) ReturnCode; +pub extern fn uv_fs_event_start(handle: *uv_fs_event_t, cb: uv_fs_event_cb, path: [*:0]const u8, flags: c_uint) ReturnCode; +/// always returns zero pub extern fn uv_fs_event_stop(handle: *uv_fs_event_t) c_int; -pub extern fn uv_fs_event_getpath(handle: *uv_fs_event_t, buffer: [*]u8, size: [*c]usize) c_int; +pub extern fn uv_fs_event_getpath(handle: *uv_fs_event_t, buffer: [*]u8, size: *usize) ReturnCode; pub extern fn uv_ip4_addr(ip: [*]const u8, port: c_int, addr: [*c]sockaddr_in) c_int; pub extern fn uv_ip6_addr(ip: [*]const u8, port: c_int, addr: ?*sockaddr_in6) c_int; pub extern fn uv_ip4_name(src: [*c]const sockaddr_in, dst: [*]u8, size: usize) c_int; diff --git a/src/js/node/fs.ts b/src/js/node/fs.ts index 417dca5f03a8a0..196aa2fecb539e 100644 --- a/src/js/node/fs.ts +++ b/src/js/node/fs.ts @@ -51,17 +51,10 @@ class FSWatcher extends EventEmitter { this.#listener = listener; try { this.#watcher = fs.watch(path, options || {}, this.#onEvent.bind(this)); - } catch (e) { - if (!e.message?.startsWith("FileNotFound")) { - throw e; - } - const notFound = new Error(`ENOENT: no such file or directory, watch '${path}'`); - notFound.code = "ENOENT"; - notFound.errno = -2; - notFound.path = path; - notFound.syscall = "watch"; - notFound.filename = path; - throw notFound; + } catch (e: any) { + e.path = path; + e.filename = path; + throw e; } } @@ -74,6 +67,12 @@ class FSWatcher extends EventEmitter { }); return; } else if (eventType === "error") { + // TODO: Next.js/watchpack causes this to emits weird EACCES errors on + // paths that shouldn't be watched. A better solution is to figure out why + // these paths get watched in the first place. For now we will rewrite the + // .code, which will cause their code path to ignore the error. + if (filenameOrError.code === "EACCES") filenameOrError.code = "EPERM"; + this.emit(eventType, filenameOrError); } else { this.emit("change", eventType, filenameOrError); diff --git a/src/sys.zig b/src/sys.zig index da7879b5c42776..6d98986145d942 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -120,6 +120,7 @@ pub const Tag = enum(u8) { futime, pidfd_open, poll, + watch, kevent, kqueue, @@ -1582,7 +1583,7 @@ pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { } } -pub fn readlink(in: [:0]const u8, buf: []u8) Maybe(usize) { +pub fn readlink(in: [:0]const u8, buf: []u8) Maybe([:0]u8) { if (comptime Environment.isWindows) { return sys_uv.readlink(in, buf); } @@ -1590,15 +1591,16 @@ pub fn readlink(in: [:0]const u8, buf: []u8) Maybe(usize) { while (true) { const rc = sys.readlink(in, buf.ptr, buf.len); - if (Maybe(usize).errnoSys(rc, .readlink)) |err| { + if (Maybe([:0]u8).errnoSys(rc, .readlink)) |err| { if (err.getErrno() == .INTR) continue; return err; } - return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; + buf[@intCast(rc)] = 0; + return .{ .result = buf[0..@intCast(rc) :0] }; } } -pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe(usize) { +pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe([:0]const u8) { while (true) { const rc = sys.readlinkat(fd, in, buf.ptr, buf.len); @@ -1606,7 +1608,8 @@ pub fn readlinkat(fd: bun.FileDescriptor, in: [:0]const u8, buf: []u8) Maybe(usi if (err.getErrno() == .INTR) continue; return err; } - return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; + buf[@intCast(rc)] = 0; + return Maybe(usize){ .result = buf[0..@intCast(rc)] }; } } @@ -1953,20 +1956,9 @@ pub fn getFdPath(fd: bun.FileDescriptor, out_buffer: *[MAX_PATH_BYTES]u8) Maybe( const proc_path = std.fmt.bufPrintZ(&procfs_buf, "/proc/self/fd/{d}", .{fd.cast()}) catch unreachable; return switch (readlink(proc_path, out_buffer)) { .err => |err| return .{ .err = err }, - .result => |len| return .{ .result = out_buffer[0..len] }, + .result => |result| .{ .result = result }, }; }, - // .solaris => { - // var procfs_buf: ["/proc/self/path/-2147483648".len:0]u8 = undefined; - // const proc_path = std.fmt.bufPrintZ(procfs_buf[0..], "/proc/self/path/{d}", .{fd}) catch unreachable; - - // const target = readlinkZ(proc_path, out_buffer) catch |err| switch (err) { - // error.UnsupportedReparsePointType => unreachable, - // error.NotLink => unreachable, - // else => |e| return e, - // }; - // return target; - // }, else => @compileError("querying for canonical path of a handle is unsupported on this host"), } } diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 4561d71040fead..e8bf926f200874 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -165,7 +165,7 @@ pub fn unlink(file_path: [:0]const u8) Maybe(void) { .{ .result = {} }; } -pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { +pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe([:0]u8) { assertIsValidWindowsPath(u8, file_path); var req: uv.fs_t = uv.fs_t.uninitialized; defer req.deinit(); @@ -176,7 +176,7 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { log("uv readlink({s}) = {d}, [err]", .{ file_path, rc.int() }); return .{ .err = .{ .errno = errno, .syscall = .readlink } }; } else { - // Seems like `rc` does not contain the errno? + // Seems like `rc` does not contain the size? bun.assert(rc.int() == 0); const slice = bun.span(req.ptrAs([*:0]u8)); if (slice.len > buf.len) { @@ -185,7 +185,8 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { } log("uv readlink({s}) = {d}, {s}", .{ file_path, rc.int(), slice }); @memcpy(buf[0..slice.len], slice); - return .{ .result = slice.len }; + buf[slice.len] = 0; + return .{ .result = buf[0..slice.len :0] }; } } diff --git a/src/watcher.zig b/src/watcher.zig index 57c5032b2c45b3..b8e6fcf998ac33 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -48,20 +48,52 @@ const INotify = struct { } }; - pub fn watchPath(this: *INotify, pathname: [:0]const u8) !EventListIndex { + pub fn watchPath(this: *INotify, pathname: [:0]const u8) bun.JSC.Maybe(EventListIndex) { bun.assert(this.loaded_inotify); const old_count = this.watch_count.fetchAdd(1, .Release); defer if (old_count == 0) Futex.wake(&this.watch_count, 10); const watch_file_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.MOVED_TO | std.os.linux.IN.MODIFY; - return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_file_mask); + return .{ + .result = std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_file_mask) catch |err| return .{ + .err = .{ + .errno = @truncate(@intFromEnum(switch (err) { + error.FileNotFound => bun.C.E.NOENT, + error.AccessDenied => bun.C.E.ACCES, + error.SystemResources => bun.C.E.NOMEM, + error.Unexpected => bun.C.E.INVAL, + error.NotDir => bun.C.E.NOTDIR, + error.NameTooLong => bun.C.E.NAMETOOLONG, + error.UserResourceLimitReached => bun.C.E.MFILE, + error.WatchAlreadyExists => bun.C.E.EXIST, + })), + .syscall = .watch, + }, + }, + }; } - pub fn watchDir(this: *INotify, pathname: [:0]const u8) !EventListIndex { + pub fn watchDir(this: *INotify, pathname: [:0]const u8) bun.JSC.Maybe(EventListIndex) { bun.assert(this.loaded_inotify); const old_count = this.watch_count.fetchAdd(1, .Release); defer if (old_count == 0) Futex.wake(&this.watch_count, 10); const watch_dir_mask = std.os.linux.IN.EXCL_UNLINK | std.os.linux.IN.DELETE | std.os.linux.IN.DELETE_SELF | std.os.linux.IN.CREATE | std.os.linux.IN.MOVE_SELF | std.os.linux.IN.ONLYDIR | std.os.linux.IN.MOVED_TO; - return std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_dir_mask); + return .{ + .result = std.os.inotify_add_watchZ(this.inotify_fd, pathname, watch_dir_mask) catch |err| return .{ + .err = .{ + .errno = @truncate(@intFromEnum(switch (err) { + error.FileNotFound => bun.C.E.NOENT, + error.AccessDenied => bun.C.E.ACCES, + error.SystemResources => bun.C.E.NOMEM, + error.Unexpected => bun.C.E.INVAL, + error.NotDir => bun.C.E.NOTDIR, + error.NameTooLong => bun.C.E.NAMETOOLONG, + error.UserResourceLimitReached => bun.C.E.MFILE, + error.WatchAlreadyExists => bun.C.E.EXIST, + })), + .syscall = .watch, + }, + }, + }; } pub fn unwatch(this: *INotify, wd: EventListIndex) void { @@ -81,22 +113,26 @@ const INotify = struct { this.inotify_fd = try std.os.inotify_init1(std.os.linux.IN.CLOEXEC); } - pub fn read(this: *INotify) ![]*const INotifyEvent { + pub fn read(this: *INotify) bun.JSC.Maybe([]*const INotifyEvent) { bun.assert(this.loaded_inotify); restart: while (true) { - Futex.wait(&this.watch_count, 0, null) catch unreachable; + Futex.wait(&this.watch_count, 0, null) catch |err| switch (err) { + error.TimedOut => unreachable, // timeout is infinite + }; + const rc = std.os.system.read( this.inotify_fd, @as([*]u8, @ptrCast(@alignCast(&this.eventlist))), @sizeOf(EventListBuffer), ); - switch (std.os.errno(rc)) { + const errno = std.os.errno(rc); + switch (errno) { .SUCCESS => { var len = @as(usize, @intCast(rc)); - if (len == 0) return &[_]*INotifyEvent{}; + if (len == 0) return .{ .result = &[_]*INotifyEvent{} }; // IN_MODIFY is very noisy // we do a 0.1ms sleep to try to coalesce events better @@ -114,15 +150,17 @@ const INotify = struct { @as([*]u8, @ptrCast(@alignCast(&this.eventlist))) + len, @sizeOf(EventListBuffer) - len, ); - switch (std.os.errno(new_rc)) { + const e = std.os.errno(new_rc); + switch (e) { .SUCCESS => { len += @as(usize, @intCast(new_rc)); }, .AGAIN => continue, .INTR => continue, - .INVAL => return error.ShortRead, - .BADF => return error.INotifyFailedToStart, - else => unreachable, + else => return .{ .err = .{ + .errno = @truncate(@intFromEnum(e)), + .syscall = .read, + } }, } break; } @@ -150,16 +188,15 @@ const INotify = struct { count += 1; } - return this.eventlist_ptrs[0..count]; + return .{ .result = this.eventlist_ptrs[0..count] }; }, .AGAIN => continue :restart, - .INVAL => return error.ShortRead, - .BADF => return error.INotifyFailedToStart, - - else => unreachable, + else => return .{ .err = .{ + .errno = @truncate(@intFromEnum(errno)), + .syscall = .read, + } }, } } - unreachable; } pub fn stop(this: *INotify) void { @@ -182,18 +219,19 @@ const DarwinWatcher = struct { eventlist: [WATCHER_MAX_LIST]KEvent = undefined, eventlist_index: EventListIndex = 0, - fd: i32 = 0, + fd: bun.FileDescriptor = bun.invalid_fd, pub fn init(this: *DarwinWatcher, _: []const u8) !void { - this.fd = try std.os.kqueue(); - if (this.fd == 0) return error.KQueueError; + const fd = try std.os.kqueue(); + if (fd == 0) return error.KQueueError; + this.fd = bun.toFD(fd); } pub fn stop(this: *DarwinWatcher) void { - if (this.fd != 0) { + if (this.fd.isValid()) { _ = bun.sys.close(this.fd); + this.fd = bun.invalid_fd; } - this.fd = 0; } }; @@ -233,13 +271,17 @@ const WindowsWatcher = struct { dirHandle: w.HANDLE, // invalidates any EventIterators - fn prepare(this: *DirWatcher) Error!void { + fn prepare(this: *DirWatcher) bun.JSC.Maybe(void) { const filter = w.FILE_NOTIFY_CHANGE_FILE_NAME | w.FILE_NOTIFY_CHANGE_DIR_NAME | w.FILE_NOTIFY_CHANGE_LAST_WRITE | w.FILE_NOTIFY_CHANGE_CREATION; if (w.kernel32.ReadDirectoryChangesW(this.dirHandle, &this.buf, this.buf.len, 1, filter, null, &this.overlapped, null) == 0) { const err = w.kernel32.GetLastError(); log("failed to start watching directory: {s}", .{@tagName(err)}); - return Error.ReadDirectoryChangesFailed; + return .{ .err = .{ + .errno = @intFromEnum(bun.C.SystemErrno.init(err) orelse bun.C.SystemErrno.EINVAL), + .syscall = .watch, + } }; } + return .{ .result = {} }; } }; @@ -323,8 +365,11 @@ const WindowsWatcher = struct { }; // wait until new events are available - pub fn next(this: *WindowsWatcher, timeout: Timeout) !?EventIterator { - try this.watcher.prepare(); + pub fn next(this: *WindowsWatcher, timeout: Timeout) bun.JSC.Maybe(?EventIterator) { + switch (this.watcher.prepare()) { + .err => |err| return .{ .err = err }, + .result => {}, + } var nbytes: w.DWORD = 0; var key: w.ULONG_PTR = 0; @@ -334,10 +379,13 @@ const WindowsWatcher = struct { if (rc == 0) { const err = w.kernel32.GetLastError(); if (err == w.Win32Error.IMEOUT) { - return null; + return .{ .result = null }; } else { log("GetQueuedCompletionStatus failed: {s}", .{@tagName(err)}); - return Error.IocpFailed; + return .{ .err = .{ + .errno = @intFromEnum(bun.C.SystemErrno.init(err) orelse bun.C.SystemErrno.EINVAL), + .syscall = .watch, + } }; } } @@ -349,12 +397,18 @@ const WindowsWatcher = struct { if (nbytes == 0) { // shutdown notification // TODO close handles? - return Error.IocpFailed; + return .{ .err = .{ + .errno = @intFromEnum(bun.C.SystemErrno.ESHUTDOWN), + .syscall = .watch, + } }; } - return EventIterator{ .watcher = &this.watcher }; + return .{ .result = EventIterator{ .watcher = &this.watcher } }; } else { log("GetQueuedCompletionStatus returned no overlapped event", .{}); - return Error.IocpFailed; + return .{ .err = .{ + .errno = @truncate(@intFromEnum(bun.C.E.INVAL)), + .syscall = .watch, + } }; } } } @@ -558,13 +612,16 @@ pub fn NewWatcher(comptime ContextType: type) type { defer Output.flush(); if (FeatureFlags.verbose_watcher) Output.prettyln("Watcher started", .{}); - this._watchLoop() catch |err| { - this.watchloop_handle = null; - this.platform.stop(); - if (this.running) { - this.ctx.onError(err); - } - }; + switch (this._watchLoop()) { + .err => |err| { + this.watchloop_handle = null; + this.platform.stop(); + if (this.running) { + this.ctx.onError(err); + } + }, + .result => {}, + } // deinit and close descriptors if needed if (this.close_descriptors) { @@ -618,9 +675,9 @@ pub fn NewWatcher(comptime ContextType: type) type { } } - fn _watchLoop(this: *Watcher) !void { + fn _watchLoop(this: *Watcher) bun.JSC.Maybe(void) { if (Environment.isMac) { - bun.assert(this.platform.fd > 0); + bun.assert(this.platform.fd.isValid()); const KEvent = std.c.Kevent; var changelist_array: [128]KEvent = std.mem.zeroes([128]KEvent); @@ -629,7 +686,7 @@ pub fn NewWatcher(comptime ContextType: type) type { defer Output.flush(); var count_ = std.os.system.kevent( - this.platform.fd, + this.platform.fd.cast(), @as([*]KEvent, changelist), 0, @as([*]KEvent, changelist), @@ -643,7 +700,7 @@ pub fn NewWatcher(comptime ContextType: type) type { const remain = 128 - count_; var timespec = std.os.timespec{ .tv_sec = 0, .tv_nsec = 100_000 }; const extra = std.os.system.kevent( - this.platform.fd, + this.platform.fd.cast(), @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), 0, @as([*]KEvent, changelist[@as(usize, @intCast(count_))..].ptr), @@ -690,7 +747,10 @@ pub fn NewWatcher(comptime ContextType: type) type { restart: while (true) { defer Output.flush(); - var events = try this.platform.read(); + var events = switch (this.platform.read()) { + .result => |result| result, + .err => |err| return .{ .err = err }, + }; if (events.len == 0) continue :restart; // TODO: is this thread safe? @@ -776,7 +836,10 @@ pub fn NewWatcher(comptime ContextType: type) type { // first wait has infinite timeout - we're waiting for the next event and don't want to spin var timeout = WindowsWatcher.Timeout.infinite; while (true) { - var iter = try this.platform.next(timeout) orelse break; + var iter = switch (this.platform.next(timeout)) { + .err => |err| return .{ .err = err }, + .result => |iter| iter orelse break, + }; // after the first wait, we want to coalesce further events but don't want to wait for them // NOTE: using a 1ms timeout would be ideal, but that actually makes the thread wait for at least 10ms more than it should // Instead we use a 0ms timeout, which may not do as much coalescing but is more responsive. @@ -849,6 +912,8 @@ pub fn NewWatcher(comptime ContextType: type) type { this.ctx.onFileUpdate(all_events, this.changed_filepaths[0 .. last_event_index + 1], this.watchlist); } } + + return .{ .result = {} }; } fn appendFileAssumeCapacity( @@ -860,20 +925,20 @@ pub fn NewWatcher(comptime ContextType: type) type { parent_hash: HashType, package_json: ?*PackageJSON, comptime copy_file_path: bool, - ) !void { + ) bun.JSC.Maybe(void) { if (comptime Environment.isWindows) { // on windows we can only watch items that are in the directory tree of the top level dir const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); if (rel == .unrelated) { Output.warn("File {s} is not in the project directory and will not be watched\n", .{file_path}); - return; + return .{ .result = {} }; } } const watchlist_id = this.watchlist.len; const file_path_: string = if (comptime copy_file_path) - bun.asByteSlice(try this.allocator.dupeZ(u8, file_path)) + bun.asByteSlice(this.allocator.dupeZ(u8, file_path) catch bun.outOfMemory()) else file_path; @@ -912,7 +977,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - this.platform.fd, + this.platform.fd.cast(), @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -926,10 +991,14 @@ pub fn NewWatcher(comptime ContextType: type) type { // buf[file_path_to_use_.len] = 0; var buf = file_path_.ptr; const slice: [:0]const u8 = buf[0..file_path_.len :0]; - item.eventlist_index = try this.platform.watchPath(slice); + item.eventlist_index = switch (this.platform.watchPath(slice)) { + .err => |err| return .{ .err = err }, + .result => |r| r, + }; } this.watchlist.appendAssumeCapacity(item); + return .{ .result = {} }; } fn appendDirectoryAssumeCapacity( @@ -938,26 +1007,28 @@ pub fn NewWatcher(comptime ContextType: type) type { file_path: string, hash: HashType, comptime copy_file_path: bool, - ) !WatchItemIndex { + ) bun.JSC.Maybe(WatchItemIndex) { if (comptime Environment.isWindows) { // on windows we can only watch items that are in the directory tree of the top level dir const rel = bun.path.isParentOrEqual(this.fs.top_level_dir, file_path); if (rel == .unrelated) { Output.warn("Directory {s} is not in the project directory and will not be watched\n", .{file_path}); - return no_watch_item; + return .{ .result = no_watch_item }; } } const fd = brk: { if (stored_fd != .zero) break :brk stored_fd; - const dir = try std.fs.cwd().openDir(file_path, .{}); - break :brk bun.toFD(dir.fd); + break :brk switch (bun.sys.openA(file_path, 0, 0)) { + .err => |err| return .{ .err = err }, + .result => |fd| fd, + }; }; const parent_hash = getHash(bun.fs.PathName.init(file_path).dirWithTrailingSlash()); const file_path_: string = if (comptime copy_file_path) - bun.asByteSlice(try this.allocator.dupeZ(u8, file_path)) + bun.asByteSlice(this.allocator.dupeZ(u8, file_path) catch bun.outOfMemory()) else file_path; @@ -1002,7 +1073,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. _ = std.os.system.kevent( - this.platform.fd, + this.platform.fd.cast(), @as([]KEvent, events[0..1]).ptr, 1, @as([]KEvent, events[0..1]).ptr, @@ -1015,11 +1086,16 @@ pub fn NewWatcher(comptime ContextType: type) type { bun.copy(u8, &buf, file_path_to_use_); buf[file_path_to_use_.len] = 0; const slice: [:0]u8 = buf[0..file_path_to_use_.len :0]; - item.eventlist_index = try this.platform.watchDir(slice); + item.eventlist_index = switch (this.platform.watchDir(slice)) { + .err => |err| return .{ .err = err }, + .result => |r| r, + }; } this.watchlist.appendAssumeCapacity(item); - return @as(WatchItemIndex, @truncate(this.watchlist.len - 1)); + return .{ + .result = @as(WatchItemIndex, @truncate(this.watchlist.len - 1)), + }; } // Below is platform-independent @@ -1034,7 +1110,7 @@ pub fn NewWatcher(comptime ContextType: type) type { package_json: ?*PackageJSON, comptime copy_file_path: bool, comptime lock: bool, - ) !void { + ) bun.JSC.Maybe(void) { if (comptime lock) this.mutex.lock(); defer if (comptime lock) this.mutex.unlock(); bun.assert(file_path.len > 1); @@ -1062,13 +1138,16 @@ pub fn NewWatcher(comptime ContextType: type) type { } } } - try this.watchlist.ensureUnusedCapacity(this.allocator, 1 + @as(usize, @intCast(@intFromBool(parent_watch_item == null)))); + this.watchlist.ensureUnusedCapacity(this.allocator, 1 + @as(usize, @intCast(@intFromBool(parent_watch_item == null)))) catch bun.outOfMemory(); if (autowatch_parent_dir) { - parent_watch_item = parent_watch_item orelse try this.appendDirectoryAssumeCapacity(dir_fd, parent_dir, parent_dir_hash, copy_file_path); + parent_watch_item = parent_watch_item orelse switch (this.appendDirectoryAssumeCapacity(dir_fd, parent_dir, parent_dir_hash, copy_file_path)) { + .err => |err| return .{ .err = err }, + .result => |r| r, + }; } - try this.appendFileAssumeCapacity( + switch (this.appendFileAssumeCapacity( fd, file_path, hash, @@ -1076,7 +1155,10 @@ pub fn NewWatcher(comptime ContextType: type) type { parent_dir_hash, package_json, copy_file_path, - ); + )) { + .err => |err| return .{ .err = err }, + .result => {}, + } if (comptime FeatureFlags.verbose_watcher) { if (strings.indexOf(file_path, this.cwd)) |i| { @@ -1085,6 +1167,8 @@ pub fn NewWatcher(comptime ContextType: type) type { Output.prettyln("Added {s} to watch list.", .{file_path}); } } + + return .{ .result = {} }; } inline fn isEligibleDirectory(this: *Watcher, dir: string) bool { @@ -1100,7 +1184,7 @@ pub fn NewWatcher(comptime ContextType: type) type { dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, - ) !void { + ) bun.JSC.Maybe(void) { return appendFileMaybeLock(this, fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, true); } @@ -1110,17 +1194,20 @@ pub fn NewWatcher(comptime ContextType: type) type { file_path: string, hash: HashType, comptime copy_file_path: bool, - ) !void { + ) bun.JSC.Maybe(void) { this.mutex.lock(); defer this.mutex.unlock(); if (this.indexOf(hash) != null) { - return; + return .{ .result = {} }; } - try this.watchlist.ensureUnusedCapacity(this.allocator, 1); + this.watchlist.ensureUnusedCapacity(this.allocator, 1) catch bun.outOfMemory(); - _ = try this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path); + return switch (this.appendDirectoryAssumeCapacity(fd, file_path, hash, copy_file_path)) { + .err => |err| .{ .err = err }, + .result => .{ .result = {} }, + }; } pub fn addFile( @@ -1132,7 +1219,7 @@ pub fn NewWatcher(comptime ContextType: type) type { dir_fd: bun.FileDescriptor, package_json: ?*PackageJSON, comptime copy_file_path: bool, - ) !void { + ) bun.JSC.Maybe(void) { // This must lock due to concurrent transpiler this.mutex.lock(); defer this.mutex.unlock(); @@ -1145,10 +1232,10 @@ pub fn NewWatcher(comptime ContextType: type) type { fds[index] = fd; } } - return; + return .{ .result = {} }; } - try this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); + return this.appendFileMaybeLock(fd, file_path, hash, loader, dir_fd, package_json, copy_file_path, false); } pub fn indexOf(this: *Watcher, hash: HashType) ?u32 { diff --git a/test/integration/next-pages/test/dev-server-puppeteer.ts b/test/integration/next-pages/test/dev-server-puppeteer.ts index 3093a17120c165..faf620e5bfba8d 100644 --- a/test/integration/next-pages/test/dev-server-puppeteer.ts +++ b/test/integration/next-pages/test/dev-server-puppeteer.ts @@ -13,7 +13,7 @@ if (process.argv.length > 2) { } const b = await launch({ - headless: (process.env.BUN_TEST_HEADLESS ?? "1") === "0", + headless: true, dumpio: true, }); diff --git a/test/integration/next-pages/test/dev-server.test.ts b/test/integration/next-pages/test/dev-server.test.ts index af02f536174966..99d063ce363645 100644 --- a/test/integration/next-pages/test/dev-server.test.ts +++ b/test/integration/next-pages/test/dev-server.test.ts @@ -1,7 +1,7 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, expect, test } from "bun:test"; import { bunEnv, bunExe } from "../../../harness"; import { Subprocess } from "bun"; -import { copyFileSync, rmSync } from "fs"; +import { copyFileSync } from "fs"; import { join } from "path"; import { StringDecoder } from "string_decoder"; import { cp, rm } from "fs/promises"; @@ -113,31 +113,41 @@ afterAll(() => { } }); -test("hot reloading works on the client (+ tailwind hmr)", async () => { - expect(dev_server).not.toBeUndefined(); - expect(baseUrl).not.toBeUndefined(); - var pid: number, exited; - let timeout = setTimeout(() => { - if (timeout && pid) { - process.kill?.(pid); - pid = 0; - - if (dev_server_pid) { - process?.kill?.(dev_server_pid); - dev_server_pid = undefined; +// Chrome for Testing doesn't support arm64 yet +// +// https://github.com/GoogleChromeLabs/chrome-for-testing/issues/1 +// https://github.com/puppeteer/puppeteer/issues/7740 +const puppeteer_unsupported = process.platform === "linux" && process.arch === "arm64"; + +test.skipIf(puppeteer_unsupported)( + "hot reloading works on the client (+ tailwind hmr)", + async () => { + expect(dev_server).not.toBeUndefined(); + expect(baseUrl).not.toBeUndefined(); + var pid: number, exited; + let timeout = setTimeout(() => { + if (timeout && pid) { + process.kill?.(pid); + pid = 0; + + if (dev_server_pid) { + process?.kill?.(dev_server_pid); + dev_server_pid = undefined; + } } - } - }, 30000).unref(); - - ({ exited, pid } = Bun.spawn([bunExe(), "test/dev-server-puppeteer.ts", baseUrl], { - cwd: root, - env: bunEnv, - stdio: ["ignore", "inherit", "inherit"], - })); - - expect(await exited).toBe(0); - pid = 0; - clearTimeout(timeout); - // @ts-expect-error - timeout = undefined; -}, 30000); + }, 30000).unref(); + + ({ exited, pid } = Bun.spawn([bunExe(), "test/dev-server-puppeteer.ts", baseUrl], { + cwd: root, + env: bunEnv, + stdio: ["ignore", "inherit", "inherit"], + })); + + expect(await exited).toBe(0); + pid = 0; + clearTimeout(timeout); + // @ts-expect-error + timeout = undefined; + }, + 30000, +); diff --git a/test/js/node/watch/fs.watch.test.ts b/test/js/node/watch/fs.watch.test.ts index 333393589c88bb..4a95fc13a69766 100644 --- a/test/js/node/watch/fs.watch.test.ts +++ b/test/js/node/watch/fs.watch.test.ts @@ -230,7 +230,7 @@ describe("fs.watch", () => { } catch (err: any) { expect(err).toBeInstanceOf(Error); expect(err.code).toBe("ENOENT"); - expect(err.syscall).toBe("watch"); + expect(err.syscall).toBe("open"); done(); } }); @@ -418,7 +418,9 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message.indexOf("AccessDenied") !== -1).toBeTrue(); + expect(err.message).toBe("Permission denied"); + expect(err.code).toBe("EACCES"); + expect(err.syscall).toBe("open"); } }); @@ -432,7 +434,9 @@ describe("fs.watch", () => { watcher.close(); expect.unreachable(); } catch (err: any) { - expect(err.message.indexOf("AccessDenied") !== -1).toBeTrue(); + expect(err.message).toBe("Permission denied"); + expect(err.code).toBe("EACCES"); + expect(err.syscall).toBe("open"); } }); });