From c9fe57fa63c3c4ce24bc2698d6f4e2daddcea9cc Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 11 Mar 2024 08:24:30 -0700 Subject: [PATCH] wip use wrapper for managing process (#8456) * WIP sync close (shows ref count bug in stream) * fix closing on PipeWriter and PipeReader * remove old todos * join * Some shell changes at least it compiles * fix some compile errors * fix ref/unref server on windows * actually use the ref count in this places * make windows compile again * more tests passing * Make shell compile again * Slowly remove some `@panic("TODO SHELL")` * Eliminate `@panic("TODO SHELL")` for BufferedWriter * Holy cleansing of `@panic("TODO SHELL")` at least it compiles now * Okay now the shell compiles, but segfaults * Fix compiler errors * more stable stream and now Content-Range pass * make windows compile again * revert stuff until the fix is actually ready * revert onDone thing * Fix buffered writer for shell * Fix buffered writer + shell/subproc.zig and windows build * Fix for #8982 got lost in the merge * Actually buffer subproc output * Fix some stuff shell * oops * fix context deinit * fix renderMissing * shell: Fix array buffer * more stable streams (#9053) fix stream ref counting * wip * Remove `@panic("TODO")` on shell event loop tasks and Redirect open flags got lost in merge * Support redirects * fixes cc @cirospaciari * Update ReadableStreamInternals.ts * Fix spurious error * Update stream.js * leak * Fix UAF cc @cirospaciari * Fix memory leaks * HOLY FUCK big refactor * misc cleanup * shell: Fix a bunch of tests * clean up * gitignore: fix ending newline * get windows compiling again * tidy * hide linker warn with icu * closeIfPossible * Better leak test * Fix forgetting to decrement reference count * Update stdio.zig * Fix shell windows build * Stupid unreachable * Woops * basic echo hi works on windows * Fix flaky test on Windows * Fix windows regression in Bun.main (#9156) * Fix windows regression in Bun.main * Handle invalid handles * Fix flaky test * Better launch config * Fixup * Make this test less flaky on Windows * Fixup * Cygwin * Support signal codes in subprocess.kill(), resolve file path * Treat null as ignore * Ignore carriage returns * Fixup * shell: Fix IOWriter bug * shell: Use custom `open()`/`openat()` * windows shell subproc works * zack commit * I think I understand WindowsStreamingWriter * fix thing * why were we doing this in tests * shell: Fix rm * shell: Add rm -rf node_modules/ test * shell: use `.runAsTest()` in some places to make it easier to determine which test failed * [autofix.ci] apply automated fixes * woopsie * Various changes * Fix * shell: abstract output task logic * shell: mkdir builtin * fixup * stuff * shell: Make writing length of 0 in IOWriter immediately resolve * shell: Implement `touch` * shell: basic `cat` working * Make it compile on windows * shell: Fix IOReader bug * [autofix.ci] apply automated fixes * fix windows kill on subprocess/process * fix dns tests to match behavior on windows (same as nodejs) * fix windows ci * again * move `close_handle` to flags in `PipeWriter` and fix shell hanging * Fix `ls` not giving non-zero exit code on error * Handle edgecase in is_atty * Fix writer.flush() when there's no data * Fix some tests * Disable uv_unref on uv_process_t on Windows, for now. * fix writer.end * fix stdout.write * fix child-process on win32 * Make this test less flaky on Windows * Add assertion * Make these the same * Make it pass on windows * Don't commit * Log the test name * Make this test less flaky on windows * Make this test less flaky on windows * Print which test is taking awhile in the runner * fixups * Fixups * Add some assertions * Bring back test concurrency * shell: bring back redirect stdin * make it compile again cc @zackradisic * initialize env map with capacity * some fixes * cleanup * oops * fix leak, fix done * fix unconsumedPromises on events * always run expect * Update child_process.test.ts * fix reading special files * Fix a test * Deflake this test * Make these comparisons easier * Won't really fix it but slightly cleaner * Update serve.test.ts * Make the checks for if the body is already used more resilient * Move this to the harness * Make this test not hang in development * Fix this test * Make the logs better * zero init some things * Make this test better * Fix readSocket * Parallelize this test * Handle EPipe and avoid big data * This was a mistake * Fix a bunch of things * Fix memory leak * Avoid sigpipe + optimize + delete dead code * Make this take less time * Make it bigger * Remove some redundant code * Update process.zig * Merge and hopefully don't breka things along teh way * Silence build warning * Uncomment on posix * Skip test on windows * windows * Cleanup test * Update * Deflake * always * less flaky test * [autofix.ci] apply automated fixes * logs * fix uaf on shell IOReader * stuff to make it work with mini event loop * fix 2 double free scenarios, support redirections on windows * shell: Make `1>&2` and `2>&1` work with libuv * yoops * Partial fix * Partial fix * fix build * fix build * ok * Make a couple shell tests pass * More logging * fix * fix * Fix build issue * more tests pass * Deflake * Deflake * Use Output.panic instead of garbled text * Formatting * Introduce `bun.sys.File`, use it for `Output.Source.StreamType`, fix nested Output.scoped() calls, use Win32 `ReadFile` API for reading when it's not a libuv file descriptor. This lets us avoid the subtle usages of `unreachable` in std.os when writing to stdout/stderr. Previously, we were initializing the libuv loop immediately at launch due to checking for the existence of a bun build --compile'd executable. When the file descriptor is not from libuv, it's just overhead to use libuv cc @paperdave, please tell me if Iany of that is incorrect or if you think this is a bad idea. * Fix closing undefined memory file descriptors in spawn cc @zackradisic * pause instead of close * Fix poorly-written test * We don't need big numbers for this test * sad workaround * fixup * Clearer error handling for this test * Fix incorrect test @electroid when ReadableStream isn't closed, hanging is the correct behavior when consuming buffered data. We cannot know if the buffered data is finished if the stream never closes. * Fix build * Remove known failing on windows * Deflake * Mark no longer failing * show all the failing tests * Sort the list of tests * fix argument handling * dont show "posix_spawn" as an error code on windows * make bun-upgrade.test.ts pass on windows * fix bunx and bun create again sorry * a * fix invalidexe because we should not be running javascript files as if they were exes * Concurrency in test runner + better logging * Revert "fix invalidexe because we should not be running javascript files as if they were exes" This reverts commit da47cf824712512df545e7fe70d25b3bf2cf102f. * WIP: Unix fixes (#9322) * wip * [autofix.ci] apply automated fixes * wip 2 * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner * Update runner.node.mjs * Update runner.node.mjs * Document some environment variables * shell: Make `Response` work with builtins * Make it compile * make pwd test pass * [autofix.ci] apply automated fixes * Fix printing garbage for source code previews * Update javascript.zig * Fix posix test failures * Fix signal dispatch cc @paperdave. Signals can be run from any thread. This causes an assertion failure when the receiving thread happens to not be the main thread. Easiest to reproduce on linux when you spawn 100 short-lived processes at once. * windows --------- Co-authored-by: cirospaciari Co-authored-by: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Co-authored-by: Zack Radisic Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Meghan Denny Co-authored-by: Zack Radisic Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dave caruso Co-authored-by: Dylan Conway --- .github/workflows/bun-linux-build.yml | 3 - .github/workflows/bun-mac-aarch64.yml | 2 + .github/workflows/bun-mac-x64-baseline.yml | 2 + .github/workflows/bun-mac-x64.yml | 2 + .github/workflows/bun-windows.yml | 13 +- .gitignore | 3 +- .vscode/launch.json | 38 +- CMakeLists.txt | 46 +- Dockerfile | 5 +- cp.js | 16 + docs/runtime/env.md | 10 + docs/runtime/shell.md | 25 +- package.json | 1 + packages/bun-inspector-frontend/bun.lockb | Bin 9984 -> 11431 bytes packages/bun-internal-test/bun.lockb | Bin 4722 -> 5117 bytes packages/bun-internal-test/package.json | 1 + packages/bun-types/bun.d.ts | 17 +- .../bun-usockets/src/eventing/epoll_kqueue.c | 10 +- packages/bun-usockets/src/eventing/libuv.c | 1 + packages/bun-uws/src/HttpContext.h | 16 +- scripts/download-zls.ps1 | 14 +- src/StandaloneModuleGraph.zig | 3 +- src/allocators.zig | 11 +- src/analytics/analytics_thread.zig | 13 +- src/async/posix_event_loop.zig | 407 +- src/async/windows_event_loop.zig | 43 +- src/baby_list.zig | 22 + src/bun.js/ConsoleObject.zig | 19 +- src/bun.js/Strong.zig | 2 +- src/bun.js/api/BunObject.zig | 2 +- src/bun.js/api/bun/dns_resolver.zig | 2 +- src/bun.js/api/bun/process.zig | 1748 ++ src/bun.js/api/bun/socket.zig | 6 +- src/bun.js/api/bun/spawn.zig | 23 +- src/bun.js/api/bun/spawn/stdio.zig | 481 + src/bun.js/api/bun/subprocess.zig | 3809 ++-- src/bun.js/api/html_rewriter.zig | 8 +- src/bun.js/api/server.zig | 117 +- src/bun.js/api/streams.classes.ts | 50 + src/bun.js/api/streams.classes.zig | 0 src/bun.js/bindings/BunDebugger.cpp | 2 + src/bun.js/bindings/BunProcess.cpp | 123 +- src/bun.js/bindings/BunString.cpp | 14 +- src/bun.js/bindings/JSDOMGlobalObject.h | 4 + src/bun.js/bindings/JSDOMWrapper.h | 2 +- src/bun.js/bindings/KeyObject.cpp | 1 + .../bindings/ScriptExecutionContext.cpp | 28 + src/bun.js/bindings/ScriptExecutionContext.h | 32 +- src/bun.js/bindings/Sink.h | 1 - src/bun.js/bindings/ZigGlobalObject.cpp | 245 +- src/bun.js/bindings/ZigGlobalObject.h | 141 +- src/bun.js/bindings/bindings.cpp | 47 +- src/bun.js/bindings/bindings.zig | 77 +- src/bun.js/bindings/bun-spawn.cpp | 6 +- src/bun.js/bindings/c-bindings.cpp | 52 +- src/bun.js/bindings/exports.zig | 21 +- .../bindings/generated_classes_list.zig | 3 + src/bun.js/bindings/headers-cpp.h | 6 +- src/bun.js/bindings/headers-replacements.zig | 1 - src/bun.js/bindings/headers.h | 38 +- src/bun.js/bindings/headers.zig | 18 +- .../bindings/webcore/JSReadableStream.cpp | 68 +- .../bindings/webcore/JSReadableStream.h | 26 + .../bindings/webcore/JSReadableStreamSource.h | 1 + .../bindings/webcore/ReadableStream.cpp | 8 +- .../bindings/webcore/ReadableStreamSource.cpp | 1 - src/bun.js/event_loop.zig | 460 +- src/bun.js/ipc.zig | 395 +- src/bun.js/javascript.zig | 78 +- src/bun.js/module_loader.zig | 12 +- src/bun.js/node/node_fs.zig | 38 +- src/bun.js/node/types.zig | 11 +- src/bun.js/rare_data.zig | 13 +- src/bun.js/test/diff_format.zig | 4 +- src/bun.js/test/pretty_format.zig | 2 +- src/bun.js/web_worker.zig | 3 +- src/bun.js/webcore.zig | 2 +- src/bun.js/webcore/blob.zig | 60 +- src/bun.js/webcore/blob/ReadFile.zig | 173 +- src/bun.js/webcore/blob/WriteFile.zig | 7 +- src/bun.js/webcore/body.zig | 141 +- src/bun.js/webcore/request.zig | 2 +- src/bun.js/webcore/response.zig | 16 +- src/bun.js/webcore/streams.zig | 4734 +++-- src/bun.zig | 107 +- src/bun_js.zig | 2 +- src/bundler.zig | 10 +- src/bundler/bundle_v2.zig | 7 +- src/c.zig | 2 + src/cli.zig | 83 +- src/cli/init_command.zig | 2 +- src/cli/package_manager_command.zig | 3 +- src/cli/run_command.zig | 20 +- src/cli/test_command.zig | 9 +- src/codegen/generate-classes.ts | 6 +- src/codegen/generate-jssink.ts | 56 +- src/darwin_c.zig | 9 +- src/deps/libuv.zig | 527 +- src/deps/uws.zig | 39 +- src/env_loader.zig | 12 +- src/fd.zig | 21 +- src/install/install.zig | 127 +- src/install/lifecycle_script_runner.zig | 629 +- src/install/semver.zig | 2 +- src/install/windows-shim/bun_shim_impl.zig | 2 +- src/io/PipeReader.zig | 1105 +- src/io/PipeWriter.zig | 1288 ++ src/io/io.zig | 12 +- src/io/io_darwin.zig | 4 +- src/io/io_linux.zig | 10 +- src/io/pipes.zig | 105 + src/io/source.zig | 164 + src/js/builtins.d.ts | 3 +- src/js/builtins/ProcessObjectInternals.ts | 4 + .../builtins/ReadableByteStreamInternals.ts | 17 +- src/js/builtins/ReadableStream.ts | 12 +- .../builtins/ReadableStreamDefaultReader.ts | 11 +- src/js/builtins/ReadableStreamInternals.ts | 185 +- src/js/node/child_process.js | 163 +- src/js/node/events.js | 3 + src/js/node/fs.js | 5 +- src/js/node/stream.js | 159 +- src/js_lexer.zig | 2 +- src/linux_c.zig | 76 +- src/main.zig | 11 +- src/meta.zig | 15 + src/output.zig | 129 +- src/panic_handler.zig | 1 + src/report.zig | 33 +- src/resolver/resolve_path.zig | 14 +- src/shell/interpreter.zig | 14484 +++++++++------- src/shell/shell.zig | 30 +- src/shell/subproc.zig | 2816 ++- src/shell/util.zig | 130 +- src/string_mutable.zig | 2 +- src/sys.zig | 371 +- src/sys_uv.zig | 46 +- src/tagged_pointer.zig | 47 +- .../__snapshots__/bun-build-api.test.ts.snap | 234 +- test/cli/install/bun-add.test.ts | 64 +- test/cli/install/bun-create.test.ts | 8 +- test/cli/install/bun-install.test.ts | 308 +- test/cli/install/bun-link.test.ts | 38 +- test/cli/install/bun-pm.test.ts | 18 +- test/cli/install/bun-remove.test.ts | 20 +- test/cli/install/bun-run.test.ts | 24 +- test/cli/install/bun-update.test.ts | 12 +- test/cli/install/bun-upgrade.test.ts | 3 +- test/cli/install/bunx.test.ts | 20 +- .../registry/bun-install-registry.test.ts | 112 +- test/cli/run/run-quote.test.ts | 1 - test/harness.ts | 85 + test/integration/sharp/sharp.test.ts | 1 - test/js/bun/dns/resolve-dns.test.ts | 21 +- test/js/bun/http/bun-server.test.ts | 5 +- test/js/bun/http/fetch-file-upload.test.ts | 26 +- test/js/bun/http/serve.test.ts | 214 +- test/js/bun/io/bun-write.test.js | 19 +- test/js/bun/io/timed-stderr-output.js | 4 + test/js/bun/net/socket-huge-fixture.js | 14 +- test/js/bun/shell/bunshell-instance.test.ts | 1 - test/js/bun/shell/bunshell.test.ts | 258 +- test/js/bun/shell/commands/rm.test.ts | 36 +- test/js/bun/shell/lazy.test.ts | 2 - test/js/bun/shell/leak.test.ts | 5 +- test/js/bun/shell/lex.test.ts | 2 - test/js/bun/shell/test_builder.ts | 18 + test/js/bun/spawn/bash-echo.sh | 3 +- .../bun/spawn/spawn-streaming-stdin.test.ts | 105 +- .../bun/spawn/spawn-streaming-stdout.test.ts | 72 +- test/js/bun/spawn/spawn.ipc.test.ts | 36 + test/js/bun/spawn/spawn.test.ts | 366 +- test/js/bun/spawn/stdin-repro.js | 11 +- test/js/bun/test/test-test.test.ts | 8 +- test/js/bun/util/filesink.test.ts | 52 +- .../child_process/child-process-stdio.test.js | 67 +- .../child_process/child_process-node.test.js | 70 +- .../node/child_process/child_process.test.ts | 112 +- .../fixtures/child-process-echo-options.js | 3 +- test/js/node/child_process/spawned-child.js | 5 +- .../js/node/crypto/crypto.key-objects.test.ts | 59 +- test/js/node/events/event-emitter.test.ts | 11 +- test/js/node/fs/fs.test.ts | 9 +- test/js/node/process/process-sleep.js | 3 + test/js/node/process/process-stdin-echo.js | 6 +- test/js/node/process/process-stdio.test.ts | 26 +- test/js/node/process/process.test.js | 32 +- test/js/node/stream/emit-readable-on-end.js | 19 + test/js/node/stream/node-stream.test.js | 45 +- .../es-module-lexer/es-module-lexer.test.ts | 10 +- .../esbuild/esbuild-child_process.test.ts | 12 +- test/js/web/console/console-log.test.ts | 39 +- test/js/web/console/console-timeLog.test.ts | 10 +- test/js/web/fetch/body.test.ts | 17 - test/js/web/fetch/fetch.test.ts | 6 +- test/js/web/streams/streams.test.js | 3 +- test/js/web/websocket/websocket.test.js | 2 +- test/regression/issue/02499.test.ts | 9 +- test/regression/issue/07500.test.ts | 13 +- test/regression/issue/08093.test.ts | 2 +- 200 files changed, 23093 insertions(+), 16963 deletions(-) create mode 100644 cp.js create mode 100644 src/bun.js/api/bun/process.zig create mode 100644 src/bun.js/api/bun/spawn/stdio.zig create mode 100644 src/bun.js/api/streams.classes.ts create mode 100644 src/bun.js/api/streams.classes.zig create mode 100644 src/io/PipeWriter.zig create mode 100644 src/io/pipes.zig create mode 100644 src/io/source.zig create mode 100644 test/js/bun/io/timed-stderr-output.js create mode 100644 test/js/bun/spawn/spawn.ipc.test.ts create mode 100644 test/js/node/process/process-sleep.js create mode 100644 test/js/node/stream/emit-readable-on-end.js diff --git a/.github/workflows/bun-linux-build.yml b/.github/workflows/bun-linux-build.yml index 52ea86e7714486..93ca4f0240d4ec 100644 --- a/.github/workflows/bun-linux-build.yml +++ b/.github/workflows/bun-linux-build.yml @@ -262,9 +262,6 @@ jobs: TLS_POSTGRES_DATABASE_URL: ${{ secrets.TLS_POSTGRES_DATABASE_URL }} # if: ${{github.event.inputs.use_bun == 'false'}} run: | - ulimit -c unlimited - ulimit -c - node packages/bun-internal-test/src/runner.node.mjs || true # - uses: actions/upload-artifact@v4 # if: steps.test.outputs.failing_tests != '' diff --git a/.github/workflows/bun-mac-aarch64.yml b/.github/workflows/bun-mac-aarch64.yml index dbeb3981df57c3..c1c045e56d483d 100644 --- a/.github/workflows/bun-mac-aarch64.yml +++ b/.github/workflows/bun-mac-aarch64.yml @@ -223,6 +223,7 @@ jobs: cmake -S $SOURCE_DIR -B $OBJ_DIR \ -G Ninja \ + -DUSE_LTO=ON \ -DCMAKE_BUILD_TYPE=Release \ -DBUN_CPP_ONLY=1 \ -DNO_CONFIGURE_DEPENDS=1 @@ -308,6 +309,7 @@ jobs: cmake $SRC_DIR \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DUSE_LTO=ON \ -DBUN_LINK_ONLY=1 \ -DBUN_ZIG_OBJ="${{ runner.temp }}/release/bun-zig.o" \ -DBUN_CPP_ARCHIVE="${{ runner.temp }}/bun-cpp-obj/bun-cpp-objects.a" \ diff --git a/.github/workflows/bun-mac-x64-baseline.yml b/.github/workflows/bun-mac-x64-baseline.yml index 051a40942b1d13..966b97c4939005 100644 --- a/.github/workflows/bun-mac-x64-baseline.yml +++ b/.github/workflows/bun-mac-x64-baseline.yml @@ -213,6 +213,7 @@ jobs: cmake -S $SOURCE_DIR -B $OBJ_DIR \ -G Ninja \ + -DUSE_LTO=ON \ -DCMAKE_BUILD_TYPE=Release \ -DBUN_CPP_ONLY=1 \ -DNO_CONFIGURE_DEPENDS=1 @@ -293,6 +294,7 @@ jobs: cd ${{runner.temp}}/link-build cmake $SRC_DIR \ -G Ninja \ + -DUSE_LTO=ON \ -DCMAKE_BUILD_TYPE=Release \ -DBUN_LINK_ONLY=1 \ -DBUN_ZIG_OBJ="${{ runner.temp }}/release/bun-zig.o" \ diff --git a/.github/workflows/bun-mac-x64.yml b/.github/workflows/bun-mac-x64.yml index ddcc4ba990d083..09e31f7f106560 100644 --- a/.github/workflows/bun-mac-x64.yml +++ b/.github/workflows/bun-mac-x64.yml @@ -212,6 +212,7 @@ jobs: cmake -S $SOURCE_DIR -B $OBJ_DIR \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DUSE_LTO=ON \ -DBUN_CPP_ONLY=1 \ -DNO_CONFIGURE_DEPENDS=1 @@ -292,6 +293,7 @@ jobs: cmake $SRC_DIR \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DUSE_LTO=ON \ -DBUN_LINK_ONLY=1 \ -DBUN_ZIG_OBJ="${{ runner.temp }}/release/bun-zig.o" \ -DBUN_CPP_ARCHIVE="${{ runner.temp }}/bun-cpp-obj/bun-cpp-objects.a" \ diff --git a/.github/workflows/bun-windows.yml b/.github/workflows/bun-windows.yml index 2be8bbd8fecab7..4e92941679d82b 100644 --- a/.github/workflows/bun-windows.yml +++ b/.github/workflows/bun-windows.yml @@ -417,6 +417,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 + - uses: secondlife/setup-cygwin@v1 + with: + packages: bash - name: Install dependencies run: | # bun install --verbose @@ -434,12 +437,12 @@ jobs: TMPDIR: ${{runner.temp}} TLS_MONGODB_DATABASE_URL: ${{ secrets.TLS_MONGODB_DATABASE_URL }} TLS_POSTGRES_DATABASE_URL: ${{ secrets.TLS_POSTGRES_DATABASE_URL }} + SHELLOPTS: igncr + BUN_PATH_BASE: ${{runner.temp}} + BUN_PATH: release/${{env.tag}}-${{ matrix.arch == 'x86_64' && 'x64' || 'aarch64' }}${{ matrix.cpu == 'nehalem' && '-baseline' || '' }}-profile/bun.exe run: | - try { - $ErrorActionPreference = "SilentlyContinue" - $null = node packages/bun-internal-test/src/runner.node.mjs ${{runner.temp}}/release/${{env.tag}}-${{ matrix.arch == 'x86_64' && 'x64' || 'aarch64' }}${{ matrix.cpu == 'nehalem' && '-baseline' || '' }}-profile/bun.exe || $true - } catch {} - $ErrorActionPreference = "Stop" + node packages/bun-internal-test/src/runner.node.mjs || true + shell: bash - uses: sarisia/actions-status-discord@v1 if: always() && steps.test.outputs.failing_tests != '' && github.event_name == 'pull_request' with: diff --git a/.gitignore b/.gitignore index 65284a94ae3032..d00b77a4bc681f 100644 --- a/.gitignore +++ b/.gitignore @@ -160,9 +160,10 @@ x64 /.cache /src/deps/libuv /build-*/ +/kcov-out .vs **/.verdaccio-db.json /test-report.md -/test-report.json \ No newline at end of file +/test-report.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 3e8ab83e74bfd5..8505007e520923 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,6 +22,21 @@ }, "console": "internalConsole" }, + { + "type": "lldb", + "request": "launch", + "name": "bun test [file] --only", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["test", "--only", "${file}"], + "cwd": "${workspaceFolder}/test", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "1", + "BUN_DEBUG_FileReader": "1" + }, + "console": "internalConsole" + }, { "type": "lldb", "request": "launch", @@ -415,9 +430,14 @@ "name": "BUN_DEBUG_QUIET_LOGS", "value": "1" }, + { + "name": "BUN_DEBUG_jest", + "value": "1" + }, + { "name": "BUN_GARBAGE_COLLECTOR_LEVEL", - "value": "2" + "value": "1" } ] }, @@ -437,6 +457,22 @@ "name": "BUN_DEBUG_QUIET_LOGS", "value": "1" }, + { + "name": "BUN_DEBUG_EventLoop", + "value": "1" + }, + { + "name": "BUN_DEBUG_uv", + "value": "1" + }, + { + "name": "BUN_DEBUG_SYS", + "value": "1" + }, + { + "name": "BUN_DEBUG_PipeWriter", + "value": "1" + }, { "name": "BUN_GARBAGE_COLLECTOR_LEVEL", "value": "2" diff --git a/CMakeLists.txt b/CMakeLists.txt index f5c5775b8edc75..1a93aa9463b2a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,11 @@ elseif(CMAKE_BUILD_TYPE STREQUAL "Release") # it is enabled for the time being to make sure to catch more bugs in the experimental windows builds set(DEFAULT_ZIG_OPTIMIZE "ReleaseSafe") else() - set(bun "bun-profile") + if(ZIG_OPTIMIZE STREQUAL "Debug") + set(bun "bun-debug") + else() + set(bun "bun-profile") + endif() endif() endif() @@ -227,6 +231,13 @@ set(DEFAULT_USE_DEBUG_JSC, OFF) if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(DEFAULT_USE_DEBUG_JSC ON) + set(DEFAULT_LTO OFF) +elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + if(CI) + set(DEFAULT_LTO ON) + else() + set(DEFAULT_LTO OFF) + endif() endif() if(WIN32) @@ -263,6 +274,8 @@ option(USE_DEBUG_JSC "Enable assertions and use a debug build of JavaScriptCore" option(USE_UNIFIED_SOURCES "Use unified sources to speed up the build" OFF) option(USE_STATIC_LIBATOMIC "Statically link libatomic, requires the presence of libatomic.a" ${DEFAULT_USE_STATIC_LIBATOMIC}) +option(USE_LTO "Enable Link-Time Optimization" ${DEFAULT_LTO}) + if(USE_VALGRIND) # Disable SIMD set(USE_BASELINE_BUILD ON) @@ -430,7 +443,13 @@ if(NOT WEBKIT_DIR) set(BUN_WEBKIT_PACKAGE_NAME_SUFFIX "-debug") set(ASSERT_ENABLED "1") elseif(NOT DEBUG AND NOT WIN32) - set(BUN_WEBKIT_PACKAGE_NAME_SUFFIX "-lto") + # Avoid waiting for LTO in local release builds outside of CI + if(USE_LTO) + set(BUN_WEBKIT_PACKAGE_NAME_SUFFIX "-lto") + else() + set(BUN_WEBKIT_PACKAGE_NAME_SUFFIX "") + endif() + set(ASSERT_ENABLED "0") endif() @@ -958,15 +977,28 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions("BUN_DEBUG=1") elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + set(LTO_FLAG "") + if(NOT WIN32) - target_compile_options(${bun} PUBLIC -O3 -flto=full -emit-llvm -g1 + if(USE_LTO) + list(APPEND LTO_FLAG "-flto=full" "-emit-llvm") + endif() + + target_compile_options(${bun} PUBLIC -O3 ${LTO_FLAG} -g1 -Werror=return-type -Werror=return-stack-address -Werror=implicit-function-declaration ) else() - target_compile_options(${bun} PUBLIC /O2 -flto=full /DEBUG /Z7) - target_link_options(${bun} PUBLIC /LTCG /DEBUG) + set(LTO_LINK_FLAG "") + + if(USE_LTO) + list(APPEND LTO_FLAG "-flto=full" "-emit-llvm") + list(APPEND LTO_LINK_FLAG "/LTCG") + endif() + + target_compile_options(${bun} PUBLIC /O2 ${LTO_FLAG} /DEBUG /Z7) + target_link_options(${bun} PUBLIC ${LTO_LINK_FLAG} /DEBUG) endif() endif() @@ -1018,11 +1050,15 @@ else() endif() if(APPLE) + # this is gated to avoid the following warning when developing on modern versions of macOS. + # ld: warning: object file (/opt/homebrew/opt/icu4c/lib/libicudata.a[2](icudt73l_dat.o)) was built for newer 'macOS' version (14.0) than being linked (11.0) + if(DEFINED ENV{CI}) if(ARCH STREQUAL "x86_64") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") else() set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") endif() + endif() target_link_options(${bun} PUBLIC "-dead_strip") target_link_options(${bun} PUBLIC "-dead_strip_dylibs") diff --git a/Dockerfile b/Dockerfile index 392db49f34739e..5df2a028bf3ee1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -372,7 +372,7 @@ ENV CCACHE_DIR=/ccache RUN --mount=type=cache,target=/ccache mkdir ${BUN_DIR}/build \ && cd ${BUN_DIR}/build \ && mkdir -p tmp_modules tmp_functions js codegen \ - && cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DUSE_DEBUG_JSC=${ASSERTIONS} -DBUN_CPP_ONLY=1 -DWEBKIT_DIR=/build/bun/bun-webkit -DCANARY=${CANARY} -DZIG_COMPILER=system \ + && cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DUSE_LTO=ON -DUSE_DEBUG_JSC=${ASSERTIONS} -DBUN_CPP_ONLY=1 -DWEBKIT_DIR=/build/bun/bun-webkit -DCANARY=${CANARY} -DZIG_COMPILER=system \ && bash compile-cpp-only.sh -v FROM bun-base-with-zig as bun-codegen-for-zig @@ -419,6 +419,7 @@ RUN mkdir -p build \ && cmake .. \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DUSE_LTO=ON \ -DZIG_OPTIMIZE="${ZIG_OPTIMIZE}" \ -DCPU_TARGET="${CPU_TARGET}" \ -DZIG_TARGET="${TRIPLET}" \ @@ -476,6 +477,7 @@ RUN cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DBUN_LINK_ONLY=1 \ -DBUN_ZIG_OBJ="${BUN_DIR}/build/bun-zig.o" \ + -DUSE_LTO=ON \ -DUSE_DEBUG_JSC=${ASSERTIONS} \ -DBUN_CPP_ARCHIVE="${BUN_DIR}/build/bun-cpp-objects.a" \ -DWEBKIT_DIR="${BUN_DIR}/bun-webkit" \ @@ -540,6 +542,7 @@ RUN cmake .. \ -DNO_CONFIGURE_DEPENDS=1 \ -DCANARY="${CANARY}" \ -DZIG_COMPILER=system \ + -DUSE_LTO=ON \ && ninja -v \ && ./bun --revision \ && mkdir -p /build/out \ diff --git a/cp.js b/cp.js new file mode 100644 index 00000000000000..4c70087c3a92a8 --- /dev/null +++ b/cp.js @@ -0,0 +1,16 @@ +const { spawn } = require("child_process"); +console.clear(); +console.log("--start--"); +const proc = spawn("sleep", ["0.5"], { stdio: ["ignore", "ignore", "ignore"] }); + +console.time("Elapsed"); +process.on("exit", () => { + console.timeEnd("Elapsed"); +}); +proc.on("exit", (code, signal) => { + console.log(`child process terminated with code ${code} and signal ${signal}`); + timer.unref(); +}); +proc.unref(); + +var timer = setTimeout(() => {}, 1000); diff --git a/docs/runtime/env.md b/docs/runtime/env.md index 5d282e0bc809b3..78c1d54ae39146 100644 --- a/docs/runtime/env.md +++ b/docs/runtime/env.md @@ -163,6 +163,16 @@ These environment variables are read by Bun and configure aspects of its behavio --- +- `BUN_CONFIG_MAX_HTTP_REQUESTS` +- Control the maximum number of concurrent HTTP requests sent by fetch and `bun install`. Defaults to `256`. If you are running into rate limits or connection issues, you can reduce this number. + +--- + +- `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD` +- If `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=1`, then `bun --watch` will not clear the console on reload + +--- + - `DO_NOT_TRACK` - Telemetry is not sent yet as of November 28th, 2023, but we are planning to add telemetry in the coming months. If `DO_NOT_TRACK=1`, then analytics are [disabled](https://do-not-track.dev/). Bun records bundle timings (so we can answer with data, "is Bun getting faster?") and feature usage (e.g., "are people actually using macros?"). The request body size is about 60 bytes, so it's not a lot of data. Equivalent of `telemetry=false` in bunfig. diff --git a/docs/runtime/shell.md b/docs/runtime/shell.md index fe0a379861051d..d544680b633325 100644 --- a/docs/runtime/shell.md +++ b/docs/runtime/shell.md @@ -400,25 +400,26 @@ await $`echo ${{ raw: '$(foo) `bar` "baz"' }}` // => baz ``` -## .bun.sh file loader +## .sh file loader -For simple shell scripts, instead of `sh`, you can use Bun Shell to run shell scripts. +For simple shell scripts, instead of `/bin/sh`, you can use Bun Shell to run shell scripts. -To do that, run any file with bun that ends with `.bun.sh`: +To do so, just run the script with `bun` on a file with the `.sh` extension. -```sh -$ echo "echo Hello World!" > script.bun.sh -$ bun ./script.bun.sh -> Hello World! +```sh#script.sh +echo "Hello World! pwd=$(pwd)" ``` -On Windows, Bun Shell is used automatically to run `.sh` files when using Bun: - ```sh -$ echo "echo Hello World!" > script.sh -# On windows, .bun.sh is not needed, just .sh $ bun ./script.sh -> Hello World! +Hello World! pwd=/home/demo +``` + +Scripts with Bun Shell are cross platform, which means they work on Windows: + +``` +PS C:\Users\Demo> bun .\script.sh +Hello World! pwd=C:\Users\Demo ``` ## Credits diff --git a/package.json b/package.json index 0849cd63573f34..53ce562291b58a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build": "if [ ! -e build ]; then bun setup; fi && ninja -C build", "build:valgrind": "cmake . -DZIG_OPTIMIZE=Debug -DUSE_DEBUG_JSC=ON -DCMAKE_BUILD_TYPE=Debug -GNinja -Bbuild-valgrind && ninja -Cbuild-valgrind", "build:release": "cmake . -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-release && ninja -Cbuild-release", + "build:debug-zig-release": "cmake . -DCMAKE_BUILD_TYPE=Release -DZIG_OPTIMIZE=Debug -GNinja -Bbuild-debug-zig-release && ninja -Cbuild-debug-zig-release", "build:safe": "cmake . -DZIG_OPTIMIZE=ReleaseSafe -DUSE_DEBUG_JSC=ON -DCMAKE_BUILD_TYPE=Release -GNinja -Bbuild-safe && ninja -Cbuild-safe", "typecheck": "tsc --noEmit && cd test && bun run typecheck", "fmt": "prettier --write --cache './{.vscode,src,test,bench,packages/{bun-types,bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}/**/*.{mjs,ts,tsx,js,jsx}'", diff --git a/packages/bun-inspector-frontend/bun.lockb b/packages/bun-inspector-frontend/bun.lockb index 9133cce88cd61685558aca8f0420c76a1d838c28..053ac067926ce392d265a461820d8da6df11a74d 100755 GIT binary patch delta 2137 zcmaJ?eN>ZG7=Pcn!8Rl|nBdsn@s-#>Ha3`vfFm?S`H*>ZN{SYOV`LyQLnLLSB_1=x zTZBN_G!!Gx0p>SHvSX%+oWQgbo=7|>rlDm=rPQG3z8{P~Jl%7)-}5}b=XvgZ@4fHu zwWlA<+@(?5^SX3>+mqXfmV4b-v$h1hF`+cE?A0{ko!YfJa$U$$0j zU)vdaG(JXD828;>Q(ZfW9*B%6Lm)z#Jfk20_Gb>t;>8M9ft5sChrmjvtp{VQVFDN1 z1QwHb7A&ULozJlgz)GcS>&IdPV5QSBoq%IEfyH(Ki+K z*xcZEa;7~C_Gn&`>Xfw2d$h8u=fkBooj`m3YKOKr@3V1@@o8{_ADZz& z>}`iFQT(N;2<;0n+hyjwsC(yPFBxwL>0K{sPj9WwEEO!*R=(4= zJ81aE*C~@$-`)5HTm?V4SbPa5J5!)Hep{UmSt`%jAlt)7EwXRLYgwJzN+F{>UWFGh zGRh2W<3+WwDS;|fBM&3x=(K#9FWz1(44BKH%Lw8EP6$SG{PR@!STW6|;XZ$}Y5ZVx z%s&+M`>Q5mS>aRyi4_Vlxgt`di_z!_3(_j&hB7F_Qnheaces+09^{iOO>km0`N$T}&g|q>w94K_wQq$i%vp$KAYmF1`!3D&*t<>Vzy#8m%Yenxfs_8gyle&?wAL zYbkWucW86|ec_bLE}m1{<|J#@I^)y^7n6k2lyXHD+-)qjVcsuAkvj$)F1`p=DCOif zR1aC4w%@L?{pBz|Zg*uJMn5R!hBIsz6gw1>zq78=#dBYaZ*EmL`S{Hpb}@s9qT~wh z>mroS3O_-9bjihgA`>Mq=fs@TW*({2L~8UgaEc%__@Zzs%_BGKVb^t)EG4S>7JTsB0ZQ%Ne@OgDnYz~b-kvc4k{4!Q;JCmw$in^ z#oFxk1=-dDEqjwucc8)-xM*zzPKv;fFhgUrY{U*-i<8m3P+t}EbeLnS3JP;_ zS+k@V(v&i0vXWwGR#F_nJQ#1`14&jfCjbBd delta 1716 zcmc&!jZ0He6u)~;om2DM`J8ie$uPw^^CL1OBfm(UN|YKxI@|m}ZRnOZg(;#4MyOXx z1ubJ_pj4z-N}ySgkp?AXRD=mZQbbf3f@$Zycb|{`0lTpMe&_c)@0@qvyYGw^wdlLG z2RtK#tS9GgN;a$Jt&)w+4*6BEl7+6KuUYU|ik| zFfPwULNp&euq1eJ%yz*>PB@>6!p+h-Y>_TZ6uTEmQhJ}edkwT{JMQ*RrL~Il$I|X? zt^{v)CFKlxD5~2}x2O1t#~dD}c#SnGzt2|iu`jXfO`#)0*8Zy!<-A`pK}DIJ+DfJP z(-RJT+?k$xr`g$K`yU#UzVsz<8_D3fMB_L`a`5&Jx0o{) z1iuHw4hqJX6_NO%qEMO{%$Nv`Gm;u-hgL{eiBzS)uR=BO86o^TRGH-l2Z#TLB~*2( z#aM4ah(B81`;(71xcMUBS(`XFj1K1q zeq#3G)JX57P4Fib+;g5(;C<>8=_&~;G`LW$(je4eG(=a{)|-uGmh*Md25YV5qS;tq iR#$1ZKoHGF6GQ4W306e~y6Losv~gNXxed1l^#29#S)_0P diff --git a/packages/bun-internal-test/bun.lockb b/packages/bun-internal-test/bun.lockb index e5fe1426254f2c20f2df50aad7a0750975f86142..93910dcc2b400dcca43aca33965ca9e75353ec1d 100755 GIT binary patch delta 905 zcmeyQ@>hL=o+kf+w;Sr1bA@BKE&cjM@VX$!SF5x&v)Z*EycI1o{CxL8t}p`_@JBhK3>_4OG;i52Z^>GgCmS?Ivy%=T!qTK#C-Q zG~dLt$sC30Z*Q*_6wjY*z!<@3Gr5z|p0Q){Nk)6lLqHWyK$WbMCo-Bdu`o{dVzOh5 zpWMl0&)7ftB#^u|nUmR`QF5{;v%O?G69YpaP%$G=69}+CD2Bkv3z_97H!yMt>;S5; z1*%{M$^e-RlN}f}CLdsKsb>drKo}$lbQu^R(;#tfs5poQS-}J4W1~UxurPwrAmuph zaRVC#vXCN+HveSlW_D!R#vnU?&XLW2zB_8pofI~Ic$n|fc4MzwiS9qS7k~3S%)|Lq zcb{WS0rLtW|LViC$p^1B+z)#4>%c3$mf0sK2sB)te3T=alab*c|39ENcqU)u*v}X= zc_ODZ6BpCubDTw!0~kd%J8->XoUFiVIN5>AkehJ<)Q?+uCm-MwnOw!AB@6V+zyA;b z@*F6T9zazI@J-&wW2M9f6asmf3y4AC!U4pfuyx=A5g`R<)Ap-*}JudHjo2r(m;m*Vu zX9|(2pX|>k$!Iq@lh0QLmWr#J?`LXHoO+dsQ44CQivWb-A^`NE16Ki*n<_k+i(hH7 w0lx&FLrG;pYOy}lh|M1Slb9x}3CJj-2^&~A9X?$iZmF-V)U;;jgo(yK#j9i<2{M2I&%|(f z>lhvg2SFC-7M7-#rZN-)g*X`)8d88XFOXJ*(xs)DARz{Wi5taPC4dahiC2>+J1|Bt z>P?=>XwO(R`6i=1=N6!Z6HpiHRusM>on|ZPgr_W>q&Vb4M+$obMFtSW`;NswL0Xh&AsFMR3HNb*3 zK*2viL4J_nM6R68JlwAsC-ZO{ax*S~M$s0Y$;rHSQvd!#0La-OuRVZD^YBjI$7>}A z3Kd|qfqlddq(P3h09pkCDIg_ElU?|9xMo0ww((7_<4c)XF-=Yto&f+wz;9^) diff --git a/packages/bun-internal-test/package.json b/packages/bun-internal-test/package.json index 20582dae989890..2f7f9f82451e14 100644 --- a/packages/bun-internal-test/package.json +++ b/packages/bun-internal-test/package.json @@ -11,6 +11,7 @@ "p-queue": "^8.0.1" }, "devDependencies": { + "@types/p-queue": "^3.2.1", "bun-types": "canary", "prettier": "^2.8.2" }, diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c8a303fb434d0d..eba540658713e0 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -4132,7 +4132,16 @@ declare module "bun" { /** * If true, the subprocess will have a hidden window. */ - // windowsHide?: boolean; + windowsHide?: boolean; + + /** + * Path to the executable to run in the subprocess. This defaults to `cmds[0]`. + * + * One use-case for this is for applications which wrap other applications or to simulate a symlink. + * + * @default cmds[0] + */ + argv0?: string; } type OptionsToSubprocess = @@ -4316,7 +4325,7 @@ declare module "bun" { * Kill the process * @param exitCode The exitCode to send to the process */ - kill(exitCode?: number): void; + kill(exitCode?: number | NodeJS.Signals): void; /** * This method will tell Bun to wait for this process to exit after you already @@ -4376,6 +4385,8 @@ declare module "bun" { * Get the resource usage information of the process (max RSS, CPU time, etc) */ resourceUsage: ResourceUsage; + + signalCode?: string; } /** @@ -4471,6 +4482,8 @@ declare module "bun" { * ``` */ cmd: string[]; + + onExit: never; }, ): SpawnOptions.OptionsToSyncSubprocess; diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 1fca4b3d704ae5..ca511e0161c592 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -287,7 +287,7 @@ int kqueue_change(int kqfd, int fd, int old_events, int new_events, void *user_d EV_SET64(&change_list[change_length++], fd, EVFILT_WRITE, (new_events & LIBUS_SOCKET_WRITABLE) ? EV_ADD : EV_DELETE, 0, 0, (uint64_t)(void*)user_data, 0, 0); } - int ret = kevent64(kqfd, change_list, change_length, NULL, 0, 0, NULL); + int ret = kevent64(kqfd, change_list, change_length, change_list, change_length, KEVENT_FLAG_ERROR_EVENTS, NULL); // ret should be 0 in most cases (not guaranteed when removing async) @@ -458,7 +458,7 @@ void us_timer_close(struct us_timer_t *timer, int fallthrough) { struct kevent64_s event; EV_SET64(&event, (uint64_t) (void*) internal_cb, EVFILT_TIMER, EV_DELETE, 0, 0, (uint64_t)internal_cb, 0, 0); - kevent64(internal_cb->loop->fd, &event, 1, NULL, 0, 0, NULL); + kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); /* (regular) sockets are the only polls which are not freed immediately */ if(fallthrough){ @@ -477,7 +477,7 @@ void us_timer_set(struct us_timer_t *t, void (*cb)(struct us_timer_t *t), int ms struct kevent64_s event; uint64_t ptr = (uint64_t)(void*)internal_cb; EV_SET64(&event, ptr, EVFILT_TIMER, EV_ADD | (repeat_ms ? 0 : EV_ONESHOT), 0, ms, (uint64_t)internal_cb, 0, 0); - kevent64(internal_cb->loop->fd, &event, 1, NULL, 0, 0, NULL); + kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); } #endif @@ -556,7 +556,7 @@ void us_internal_async_close(struct us_internal_async *a) { struct kevent64_s event; uint64_t ptr = (uint64_t)(void*)internal_cb; EV_SET64(&event, ptr, EVFILT_MACHPORT, EV_DELETE, 0, 0, (uint64_t)(void*)internal_cb, 0,0); - kevent64(internal_cb->loop->fd, &event, 1, NULL, 0, 0, NULL); + kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); mach_port_deallocate(mach_task_self(), internal_cb->port); us_free(internal_cb->machport_buf); @@ -584,7 +584,7 @@ void us_internal_async_set(struct us_internal_async *a, void (*cb)(struct us_int event.ext[1] = MACHPORT_BUF_LEN; event.udata = (uint64_t)(void*)internal_cb; - int ret = kevent64(internal_cb->loop->fd, &event, 1, NULL, 0, 0, NULL); + int ret = kevent64(internal_cb->loop->fd, &event, 1, &event, 1, KEVENT_FLAG_ERROR_EVENTS, NULL); if (UNLIKELY(ret == -1)) { abort(); diff --git a/packages/bun-usockets/src/eventing/libuv.c b/packages/bun-usockets/src/eventing/libuv.c index e57f0164d35c58..b5c3d8c8d2d274 100644 --- a/packages/bun-usockets/src/eventing/libuv.c +++ b/packages/bun-usockets/src/eventing/libuv.c @@ -197,6 +197,7 @@ void us_loop_free(struct us_loop_t *loop) { void us_loop_run(struct us_loop_t *loop) { us_loop_integrate(loop); + uv_update_time(loop->uv_loop); uv_run(loop->uv_loop, UV_RUN_ONCE); } diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index a2a5cce0369b62..7521953ecce7c6 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -1,3 +1,4 @@ +// clang-format off /* * Authored by Alex Hultman, 2018-2020. * Intellectual property of third-party. @@ -497,12 +498,23 @@ struct HttpContext { /* Listen to port using this HttpContext */ us_listen_socket_t *listen(const char *host, int port, int options) { - return us_socket_context_listen(SSL, getSocketContext(), host, port, options, sizeof(HttpResponseData)); + auto socket = us_socket_context_listen(SSL, getSocketContext(), host, port, options, sizeof(HttpResponseData)); + // we dont depend on libuv ref for keeping it alive + if (socket) { + us_socket_unref(&socket->s); + } + return socket; } /* Listen to unix domain socket using this HttpContext */ us_listen_socket_t *listen_unix(const char *path, size_t pathlen, int options) { - return us_socket_context_listen_unix(SSL, getSocketContext(), path, pathlen, options, sizeof(HttpResponseData)); + auto* socket = us_socket_context_listen_unix(SSL, getSocketContext(), path, pathlen, options, sizeof(HttpResponseData)); + // we dont depend on libuv ref for keeping it alive + if (socket) { + us_socket_unref(&socket->s); + } + + return socket; } }; diff --git a/scripts/download-zls.ps1 b/scripts/download-zls.ps1 index 78e84dbdc585ad..1daf10381eaf88 100644 --- a/scripts/download-zls.ps1 +++ b/scripts/download-zls.ps1 @@ -1,7 +1,7 @@ -push-location .cache -try { - git clone https://github.com/zigtools/zls - set-location zls - git checkout 62f17abe283bfe0ff2710c380c620a5a6e413996 - ..\zig\zig.exe build -Doptimize=ReleaseFast -} finally { Pop-Location } +push-location .cache +try { + git clone https://github.com/zigtools/zls + set-location zls + git checkout a6786e1c324d773f9315f44c0ad976ef192d5493 + ..\zig\zig.exe build -Doptimize=ReleaseFast +} finally { Pop-Location } diff --git a/src/StandaloneModuleGraph.zig b/src/StandaloneModuleGraph.zig index f521325e4b7e71..aab3c3e48ceb71 100644 --- a/src/StandaloneModuleGraph.zig +++ b/src/StandaloneModuleGraph.zig @@ -570,7 +570,8 @@ pub const StandaloneModuleGraph = struct { } pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph { - const self_exe = bun.toLibUVOwnedFD(openSelf() catch return null); + // Do not invoke libuv here. + const self_exe = openSelf() catch return null; defer _ = Syscall.close(self_exe); var trailer_bytes: [4096]u8 = undefined; diff --git a/src/allocators.zig b/src/allocators.zig index f8f3ebcc35469c..cea0ae9b41e511 100644 --- a/src/allocators.zig +++ b/src/allocators.zig @@ -5,12 +5,17 @@ const Environment = @import("./env.zig"); const FixedBufferAllocator = std.heap.FixedBufferAllocator; const bun = @import("root").bun; -/// Checks if a slice's pointer is contained within another slice. -pub inline fn isSliceInBuffer(comptime T: type, slice: []const T, buffer: []const T) bool { +pub fn isSliceInBufferT(comptime T: type, slice: []const T, buffer: []const T) bool { return (@intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and (@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)); } +/// Checks if a slice's pointer is contained within another slice. +/// If you need to make this generic, use isSliceInBufferT. +pub fn isSliceInBuffer(slice: []const u8, buffer: []const u8) bool { + return isSliceInBufferT(u8, slice, buffer); +} + pub fn sliceRange(slice: []const u8, buffer: []const u8) ?[2]u32 { return if (@intFromPtr(buffer.ptr) <= @intFromPtr(slice.ptr) and (@intFromPtr(slice.ptr) + slice.len) <= (@intFromPtr(buffer.ptr) + buffer.len)) @@ -309,7 +314,7 @@ pub fn BSSStringList(comptime _count: usize, comptime _item_length: usize) type } pub fn exists(self: *const Self, value: ValueType) bool { - return isSliceInBuffer(u8, value, &self.backing_buf); + return isSliceInBuffer(value, &self.backing_buf); } pub fn editableSlice(slice: []const u8) []u8 { diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index f284a11914ced3..dd3fe965de7aa8 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -277,6 +277,8 @@ pub const GenerateHeader = struct { var platform_: ?Analytics.Platform = null; pub const Platform = Analytics.Platform; + var linux_kernel_version: Semver.Version = undefined; + pub fn forOS() Analytics.Platform { if (platform_ != null) return platform_.?; @@ -285,6 +287,11 @@ pub const GenerateHeader = struct { return platform_.?; } else if (comptime Environment.isPosix) { platform_ = forLinux(); + + const release = bun.sliceTo(&linux_os_name.release, 0); + const sliced_string = Semver.SlicedString.init(release, release); + const result = Semver.Version.parse(sliced_string); + linux_kernel_version = result.version.min(); } else { platform_ = Platform{ .os = Analytics.OperatingSystem.windows, @@ -301,11 +308,9 @@ pub const GenerateHeader = struct { @compileError("This function is only implemented on Linux"); } _ = forOS(); - const release = bun.sliceTo(&linux_os_name.release, 0); - const sliced_string = Semver.SlicedString.init(release, release); - const result = Semver.Version.parse(sliced_string); + // we only care about major, minor, patch so we don't care about the string - return result.version.min(); + return linux_kernel_version; } pub fn forLinux() Analytics.Platform { diff --git a/src/async/posix_event_loop.zig b/src/async/posix_event_loop.zig index 2ad65e05df79a8..1becd1c7cc5200 100644 --- a/src/async/posix_event_loop.zig +++ b/src/async/posix_event_loop.zig @@ -50,11 +50,15 @@ pub const KeepAlive = struct { /// Prevent a poll from keeping the process alive. pub fn unref(this: *KeepAlive, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); if (this.status != .active) return; this.status = .inactive; - // vm.event_loop_handle.?.subActive(1); + + if (comptime @TypeOf(event_loop_ctx_) == JSC.EventLoopHandle) { + event_loop_ctx_.loop().subActive(1); + return; + } + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); event_loop_ctx.platformEventLoop().subActive(1); } @@ -88,12 +92,17 @@ pub const KeepAlive = struct { /// Allow a poll to keep the process alive. pub fn ref(this: *KeepAlive, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); if (this.status != .inactive) return; + this.status = .active; + const EventLoopContext = @TypeOf(event_loop_ctx_); + if (comptime EventLoopContext == JSC.EventLoopHandle) { + event_loop_ctx_.ref(); + return; + } + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); event_loop_ctx.platformEventLoop().ref(); - // vm.event_loop_handle.?.ref(); } /// Allow a poll to keep the process alive. @@ -121,7 +130,7 @@ pub const FilePoll = struct { fd: bun.FileDescriptor = invalid_fd, flags: Flags.Set = Flags.Set{}, - owner: Owner = undefined, + owner: Owner = Owner.Null, /// We re-use FilePoll objects to avoid allocating new ones. /// @@ -131,63 +140,64 @@ pub const FilePoll = struct { generation_number: KQueueGenerationNumber = 0, next_to_free: ?*FilePoll = null, - event_loop_kind: JSC.EventLoopKind = .js, + allocator_type: AllocatorType = .js, - const FileReader = JSC.WebCore.FileReader; - const FileSink = JSC.WebCore.FileSink; - const FileSinkMini = JSC.WebCore.FileSinkMini; - const FIFO = JSC.WebCore.FIFO; - const FIFOMini = JSC.WebCore.FIFOMini; - const ShellSubprocess = bun.ShellSubprocess; - const ShellSubprocessMini = bun.shell.SubprocessMini; - const ShellBufferedWriter = bun.shell.Interpreter.BufferedWriter; - const ShellBufferedWriterMini = bun.shell.InterpreterMini.BufferedWriter; - const ShellBufferedInput = bun.ShellSubprocess.BufferedInput; - const ShellBufferedInputMini = bun.shell.SubprocessMini.BufferedInput; - const ShellSubprocessCapturedBufferedWriter = bun.ShellSubprocess.BufferedOutput.CapturedBufferedWriter; - const ShellSubprocessCapturedBufferedWriterMini = bun.shell.SubprocessMini.BufferedOutput.CapturedBufferedWriter; - const ShellBufferedOutput = bun.shell.Subprocess.BufferedOutput; - const ShellBufferedOutputMini = bun.shell.SubprocessMini.BufferedOutput; + const ShellBufferedWriter = bun.shell.Interpreter.IOWriter.Poll; + // const ShellBufferedWriter = bun.shell.Interpreter.WriterImpl; + const FileReader = JSC.WebCore.FileReader; + // const FIFO = JSC.WebCore.FIFO; + // const FIFOMini = JSC.WebCore.FIFOMini; + + const ShellSubprocessCapturedPipeWriter = bun.shell.subproc.PipeReader.CapturedWriter.Poll; + // const ShellBufferedWriterMini = bun.shell.InterpreterMini.BufferedWriter; + // const ShellBufferedInput = bun.shell.ShellSubprocess.BufferedInput; + // const ShellBufferedInputMini = bun.shell.SubprocessMini.BufferedInput; + // const ShellSubprocessCapturedBufferedWriter = bun.shell.ShellSubprocess.BufferedOutput.CapturedBufferedWriter; + // const ShellSubprocessCapturedBufferedWriterMini = bun.shell.SubprocessMini.BufferedOutput.CapturedBufferedWriter; + // const ShellBufferedOutput = bun.shell.Subprocess.BufferedOutput; + // const ShellBufferedOutputMini = bun.shell.SubprocessMini.BufferedOutput; + const Process = bun.spawn.Process; const Subprocess = JSC.Subprocess; - const BufferedInput = Subprocess.BufferedInput; - const BufferedOutput = Subprocess.BufferedOutput; + const StaticPipeWriter = Subprocess.StaticPipeWriter.Poll; + const ShellStaticPipeWriter = bun.shell.ShellSubprocess.StaticPipeWriter.Poll; + const FileSink = JSC.WebCore.FileSink.Poll; const DNSResolver = JSC.DNS.DNSResolver; const GetAddrInfoRequest = JSC.DNS.GetAddrInfoRequest; - const Deactivated = opaque { - pub var owner: Owner = Owner.init(@as(*Deactivated, @ptrFromInt(@as(usize, 0xDEADBEEF)))); - }; - const LifecycleScriptSubprocessOutputReader = bun.install.LifecycleScriptSubprocess.OutputReader; - const LifecycleScriptSubprocessPid = bun.install.LifecycleScriptSubprocess.PidPollData; - + const BufferedReader = bun.io.BufferedReader; pub const Owner = bun.TaggedPointerUnion(.{ - FileReader, FileSink, - FileSinkMini, - Subprocess, - - ShellSubprocess, - ShellSubprocessMini, - ShellBufferedWriter, - ShellBufferedWriterMini, - ShellBufferedInput, - ShellBufferedInputMini, - ShellSubprocessCapturedBufferedWriter, - ShellSubprocessCapturedBufferedWriterMini, - ShellBufferedOutput, - ShellBufferedOutputMini, - - BufferedInput, - FIFO, - FIFOMini, - Deactivated, + + // ShellBufferedWriter, + // ShellBufferedWriterMini, + // ShellBufferedInput, + // ShellBufferedInputMini, + // ShellSubprocessCapturedBufferedWriter, + // ShellSubprocessCapturedBufferedWriterMini, + // ShellBufferedOutput, + // ShellBufferedOutputMini, + + StaticPipeWriter, + ShellStaticPipeWriter, + + // ShellBufferedWriter, + ShellSubprocessCapturedPipeWriter, + + BufferedReader, + DNSResolver, GetAddrInfoRequest, - LifecycleScriptSubprocessOutputReader, - LifecycleScriptSubprocessPid, + // LifecycleScriptSubprocessOutputReader, + Process, + ShellBufferedWriter, // i do not know why, but this has to be here otherwise compiler will complain about dependency loop }); + pub const AllocatorType = enum { + js, + mini, + }; + fn updateFlags(poll: *FilePoll, updated: Flags.Set) void { var flags = poll.flags; flags.remove(.readable); @@ -201,11 +211,31 @@ pub const FilePoll = struct { poll.flags = flags; } + pub fn format(poll: *const FilePoll, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print("FilePoll(fd={}, generation_number={d}) = {}", .{ poll.fd, poll.generation_number, Flags.Formatter{ .data = poll.flags } }); + } + + pub fn fileType(poll: *const FilePoll) bun.io.FileType { + const flags = poll.flags; + + if (flags.contains(.socket)) { + return .socket; + } + + if (flags.contains(.nonblocking)) { + return .nonblocking_pipe; + } + + return .pipe; + } + pub fn onKQueueEvent(poll: *FilePoll, _: *Loop, kqueue_event: *const std.os.system.kevent64_s) void { + poll.updateFlags(Flags.fromKQueueEvent(kqueue_event.*)); + log("onKQueueEvent: {}", .{poll}); + if (KQueueGenerationNumber != u0) std.debug.assert(poll.generation_number == kqueue_event.ext[0]); - poll.updateFlags(Flags.fromKQueueEvent(kqueue_event.*)); poll.onUpdate(kqueue_event.data); } @@ -243,7 +273,7 @@ pub const FilePoll = struct { } pub fn deinit(this: *FilePoll) void { - switch (this.event_loop_kind) { + switch (this.allocator_type) { .js => { const vm = JSC.VirtualMachine.get(); const handle = JSC.AbstractVM(vm); @@ -264,7 +294,7 @@ pub const FilePoll = struct { } pub fn deinitForceUnregister(this: *FilePoll) void { - switch (this.event_loop_kind) { + switch (this.allocator_type) { .js => { var vm = JSC.VirtualMachine.get(); const loop = vm.event_loop_handle.?; @@ -281,7 +311,7 @@ pub const FilePoll = struct { fn deinitPossiblyDefer(this: *FilePoll, vm: anytype, loop: *Loop, polls: *FilePoll.Store, force_unregister: bool) void { _ = this.unregister(loop, force_unregister); - this.owner = Deactivated.owner; + this.owner.clear(); const was_ever_registered = this.flags.contains(.was_ever_registered); this.flags = Flags.Set{}; this.fd = invalid_fd; @@ -306,58 +336,65 @@ pub const FilePoll = struct { poll.flags.insert(.needs_rearm); } - var ptr = poll.owner; - switch (ptr.tag()) { - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(FIFO))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) FIFO", .{poll.fd}); - ptr.as(FIFO).ready(size_or_offset, poll.flags.contains(.hup)); - }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedInput))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedInput", .{poll.fd}); - ptr.as(ShellBufferedInput).onPoll(size_or_offset, 0); - }, - @field(Owner.Tag, "Subprocess") => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) Subprocess", .{poll.fd}); - var loader = ptr.as(JSC.Subprocess); + const ptr = poll.owner; + std.debug.assert(!ptr.isNull()); - loader.onExitNotificationTask(); - }, + switch (ptr.tag()) { + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(FIFO))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) FIFO", .{poll.fd}); + // ptr.as(FIFO).ready(size_or_offset, poll.flags.contains(.hup)); + // }, + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedInput))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedInput", .{poll.fd}); + // ptr.as(ShellBufferedInput).onPoll(size_or_offset, 0); + // }, + + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedWriter))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedWriter", .{poll.fd}); + // var loader = ptr.as(ShellBufferedWriter); + // loader.onPoll(size_or_offset, 0); + // }, + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedWriterMini))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedWriterMini", .{poll.fd}); + // var loader = ptr.as(ShellBufferedWriterMini); + // loader.onPoll(size_or_offset, 0); + // }, + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocessCapturedBufferedWriter))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocessCapturedBufferedWriter", .{poll.fd}); + // var loader = ptr.as(ShellSubprocessCapturedBufferedWriter); + // loader.onPoll(size_or_offset, 0); + // }, + // @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocessCapturedBufferedWriterMini))) => { + // log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocessCapturedBufferedWriterMini", .{poll.fd}); + // var loader = ptr.as(ShellSubprocessCapturedBufferedWriterMini); + // loader.onPoll(size_or_offset, 0); + // }, @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedWriter))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedWriter", .{poll.fd}); - var loader = ptr.as(ShellBufferedWriter); - loader.onPoll(size_or_offset, 0); + var handler: *ShellBufferedWriter = ptr.as(ShellBufferedWriter); + handler.onPoll(size_or_offset, poll.flags.contains(.hup)); }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellBufferedWriterMini))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellBufferedWriterMini", .{poll.fd}); - var loader = ptr.as(ShellBufferedWriterMini); - loader.onPoll(size_or_offset, 0); + @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellStaticPipeWriter))) => { + var handler: *ShellStaticPipeWriter = ptr.as(ShellStaticPipeWriter); + handler.onPoll(size_or_offset, poll.flags.contains(.hup)); }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocessCapturedBufferedWriter))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocessCapturedBufferedWriter", .{poll.fd}); - var loader = ptr.as(ShellSubprocessCapturedBufferedWriter); - loader.onPoll(size_or_offset, 0); + @field(Owner.Tag, bun.meta.typeBaseName(@typeName(StaticPipeWriter))) => { + var handler: *StaticPipeWriter = ptr.as(StaticPipeWriter); + handler.onPoll(size_or_offset, poll.flags.contains(.hup)); }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocessCapturedBufferedWriterMini))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocessCapturedBufferedWriterMini", .{poll.fd}); - var loader = ptr.as(ShellSubprocessCapturedBufferedWriterMini); - loader.onPoll(size_or_offset, 0); + @field(Owner.Tag, bun.meta.typeBaseName(@typeName(FileSink))) => { + var handler: *FileSink = ptr.as(FileSink); + handler.onPoll(size_or_offset, poll.flags.contains(.hup)); }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocess))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocess", .{poll.fd}); - var loader = ptr.as(ShellSubprocess); - - loader.onExitNotificationTask(); + @field(Owner.Tag, bun.meta.typeBaseName(@typeName(BufferedReader))) => { + log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) Reader", .{poll.fd}); + var handler: *BufferedReader = ptr.as(BufferedReader); + handler.onPoll(size_or_offset, poll.flags.contains(.hup)); }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocessMini))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) ShellSubprocessMini", .{poll.fd}); - var loader = ptr.as(ShellSubprocessMini); + @field(Owner.Tag, bun.meta.typeBaseName(@typeName(Process))) => { + log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) Process", .{poll.fd}); + var loader = ptr.as(Process); - loader.onExitNotificationTask(); - }, - @field(Owner.Tag, bun.meta.typeBaseName(@typeName(JSC.WebCore.FileSink))) => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) FileSink", .{poll.fd}); - var loader = ptr.as(JSC.WebCore.FileSink); - loader.onPoll(size_or_offset, 0); + loader.onWaitPidFromEventLoopTask(); }, @field(Owner.Tag, "DNSResolver") => { @@ -376,17 +413,6 @@ pub const FilePoll = struct { loader.onMachportChange(); }, - @field(Owner.Tag, "OutputReader") => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) OutputReader", .{poll.fd}); - var output: *LifecycleScriptSubprocessOutputReader = ptr.as(LifecycleScriptSubprocessOutputReader); - output.onPoll(size_or_offset); - }, - @field(Owner.Tag, "PidPollData") => { - log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) LifecycleScriptSubprocess Pid", .{poll.fd}); - var loader: *bun.install.LifecycleScriptSubprocess = @ptrCast(ptr.as(LifecycleScriptSubprocessPid)); - loader.onProcessUpdate(size_or_offset); - }, - else => { const possible_name = Owner.typeNameFromTag(@intFromEnum(ptr.tag())); log("onUpdate " ++ kqueue_or_epoll ++ " (fd: {}) disconnected? (maybe: {s})", .{ poll.fd, possible_name orelse "" }); @@ -435,6 +461,11 @@ pub const FilePoll = struct { was_ever_registered, ignore_updates, + /// Was O_NONBLOCK set on the file descriptor? + nonblock, + + socket, + pub fn poll(this: Flags) Flags { return switch (this) { .readable => .poll_readable, @@ -448,27 +479,35 @@ pub const FilePoll = struct { pub const Set = std.EnumSet(Flags); pub const Struct = std.enums.EnumFieldStruct(Flags, bool, false); + pub const Formatter = std.fmt.Formatter(Flags.format); + + pub fn format(this: Flags.Set, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + var iter = this.iterator(); + var is_first = true; + while (iter.next()) |flag| { + if (!is_first) { + try writer.print(" | ", .{}); + } + try writer.writeAll(@tagName(flag)); + is_first = false; + } + } + pub fn fromKQueueEvent(kqueue_event: std.os.system.kevent64_s) Flags.Set { var flags = Flags.Set{}; if (kqueue_event.filter == std.os.system.EVFILT_READ) { flags.insert(Flags.readable); - log("readable", .{}); if (kqueue_event.flags & std.os.system.EV_EOF != 0) { flags.insert(Flags.hup); - log("hup", .{}); } } else if (kqueue_event.filter == std.os.system.EVFILT_WRITE) { flags.insert(Flags.writable); - log("writable", .{}); if (kqueue_event.flags & std.os.system.EV_EOF != 0) { flags.insert(Flags.hup); - log("hup", .{}); } } else if (kqueue_event.filter == std.os.system.EVFILT_PROC) { - log("proc", .{}); flags.insert(Flags.process); } else if (kqueue_event.filter == std.os.system.EVFILT_MACHPORT) { - log("machport", .{}); flags.insert(Flags.machport); } return flags; @@ -478,19 +517,15 @@ pub const FilePoll = struct { var flags = Flags.Set{}; if (epoll.events & std.os.linux.EPOLL.IN != 0) { flags.insert(Flags.readable); - log("readable", .{}); } if (epoll.events & std.os.linux.EPOLL.OUT != 0) { flags.insert(Flags.writable); - log("writable", .{}); } if (epoll.events & std.os.linux.EPOLL.ERR != 0) { flags.insert(Flags.eof); - log("eof", .{}); } if (epoll.events & std.os.linux.EPOLL.HUP != 0) { flags.insert(Flags.hup); - log("hup", .{}); } return flags; } @@ -529,10 +564,6 @@ pub const FilePoll = struct { } pub fn put(this: *Store, poll: *FilePoll, vm: anytype, ever_registered: bool) void { - if (@TypeOf(vm) != *JSC.VirtualMachine and @TypeOf(vm) != *JSC.MiniEventLoop) { - @compileError("Bad vm: " ++ @typeName(@TypeOf(vm))); - } - if (!ever_registered) { this.hive.put(poll); return; @@ -559,7 +590,7 @@ pub const FilePoll = struct { } }; - const log = Output.scoped(.FilePoll, false); + const log = bun.sys.syslog; pub inline fn isActive(this: *const FilePoll) bool { return this.flags.contains(.has_incremented_poll_count); @@ -572,10 +603,20 @@ pub const FilePoll = struct { /// This decrements the active counter if it was previously incremented /// "active" controls whether or not the event loop should potentially idle pub fn disableKeepingProcessAlive(this: *FilePoll, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); - // log("{x} disableKeepingProcessAlive", .{@intFromPtr(this)}); - // vm.event_loop_handle.?.subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); - event_loop_ctx.platformEventLoop().subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); + if (comptime @TypeOf(event_loop_ctx_) == *JSC.EventLoop) { + disableKeepingProcessAlive(this, JSC.EventLoopHandle.init(event_loop_ctx_)); + return; + } + + if (comptime @TypeOf(event_loop_ctx_) == JSC.EventLoopHandle) { + event_loop_ctx_.loop().subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); + } else { + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); + // log("{x} disableKeepingProcessAlive", .{@intFromPtr(this)}); + // vm.event_loop_handle.?.subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); + event_loop_ctx.platformEventLoop().subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); + } + this.flags.remove(.keeps_event_loop_alive); this.flags.remove(.has_incremented_active_count); } @@ -584,14 +625,29 @@ pub const FilePoll = struct { return this.flags.contains(.keeps_event_loop_alive) and this.flags.contains(.has_incremented_poll_count); } + pub fn setKeepingProcessAlive(this: *FilePoll, event_loop_ctx_: anytype, value: bool) void { + if (value) { + this.enableKeepingProcessAlive(event_loop_ctx_); + } else { + this.disableKeepingProcessAlive(event_loop_ctx_); + } + } pub fn enableKeepingProcessAlive(this: *FilePoll, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); - // log("{x} enableKeepingProcessAlive", .{@intFromPtr(this)}); + if (comptime @TypeOf(event_loop_ctx_) == *JSC.EventLoop) { + enableKeepingProcessAlive(this, JSC.EventLoopHandle.init(event_loop_ctx_)); + return; + } + if (this.flags.contains(.closed)) return; - // vm.event_loop_handle.?.addActive(@as(u32, @intFromBool(!this.flags.contains(.has_incremented_active_count)))); - event_loop_ctx.platformEventLoop().addActive(@as(u32, @intFromBool(!this.flags.contains(.has_incremented_active_count)))); + if (comptime @TypeOf(event_loop_ctx_) == JSC.EventLoopHandle) { + event_loop_ctx_.loop().addActive(@as(u32, @intFromBool(!this.flags.contains(.has_incremented_active_count)))); + } else { + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); + event_loop_ctx.platformEventLoop().addActive(@as(u32, @intFromBool(!this.flags.contains(.has_incremented_active_count)))); + } + this.flags.insert(.keeps_event_loop_alive); this.flags.insert(.has_incremented_active_count); } @@ -620,6 +676,26 @@ pub const FilePoll = struct { } pub fn init(vm: anytype, fd: bun.FileDescriptor, flags: Flags.Struct, comptime Type: type, owner: *Type) *FilePoll { + if (comptime @TypeOf(vm) == *bun.install.PackageManager) { + return init(JSC.EventLoopHandle.init(&vm.event_loop), fd, flags, Type, owner); + } + + if (comptime @TypeOf(vm) == JSC.EventLoopHandle) { + var poll = vm.filePolls().get(); + poll.fd = fd; + poll.flags = Flags.Set.init(flags); + poll.owner = Owner.init(owner); + poll.next_to_free = null; + poll.allocator_type = if (vm == .js) .js else .mini; + + if (KQueueGenerationNumber != u0) { + max_generation_number +%= 1; + poll.generation_number = max_generation_number; + } + log("FilePoll.init(0x{x}, generation_number={d}, fd={})", .{ @intFromPtr(poll), poll.generation_number, fd }); + return poll; + } + return initWithOwner(vm, fd, flags, Owner.init(owner)); } @@ -630,33 +706,14 @@ pub const FilePoll = struct { poll.flags = Flags.Set.init(flags); poll.owner = owner; poll.next_to_free = null; - poll.event_loop_kind = if (comptime @TypeOf(vm_) == *JSC.VirtualMachine) .js else .mini; - - if (KQueueGenerationNumber != u0) { - max_generation_number +%= 1; - poll.generation_number = max_generation_number; - } - return poll; - } - - pub fn initWithPackageManager(m: *bun.PackageManager, fd: bun.FileDescriptor, flags: Flags.Struct, owner: anytype) *FilePoll { - return initWithPackageManagerWithOwner(m, fd, flags, Owner.init(owner)); - } - - pub fn initWithPackageManagerWithOwner(manager: *bun.PackageManager, fd: bun.FileDescriptor, flags: Flags.Struct, owner: Owner) *FilePoll { - var poll = manager.file_poll_store.get(); - poll.fd = fd; - poll.flags = Flags.Set.init(flags); - poll.owner = owner; - poll.next_to_free = null; - // Well I'm not sure what to put here because it looks bun install doesn't use JSC event loop or mini event loop - poll.event_loop_kind = .js; + poll.allocator_type = if (comptime @TypeOf(vm_) == *JSC.VirtualMachine) .js else .mini; if (KQueueGenerationNumber != u0) { max_generation_number +%= 1; poll.generation_number = max_generation_number; } + log("FilePoll.initWithOwner(0x{x}, generation_number={d}, fd={})", .{ @intFromPtr(poll), poll.generation_number, fd }); return poll; } @@ -714,7 +771,6 @@ pub const FilePoll = struct { const Pollable = bun.TaggedPointerUnion(.{ FilePoll, - Deactivated, }); comptime { @@ -725,17 +781,20 @@ pub const FilePoll = struct { const kevent = std.c.kevent; const linux = std.os.linux; + pub const OneShotFlag = enum { dispatch, one_shot, none }; + pub fn register(this: *FilePoll, loop: *Loop, flag: Flags, one_shot: bool) JSC.Maybe(void) { - return registerWithFd(this, loop, flag, one_shot, this.fd); + return registerWithFd(this, loop, flag, if (one_shot) .one_shot else .none, this.fd); } - pub fn registerWithFd(this: *FilePoll, loop: *Loop, flag: Flags, one_shot: bool, fd: bun.FileDescriptor) JSC.Maybe(void) { + + pub fn registerWithFd(this: *FilePoll, loop: *Loop, flag: Flags, one_shot: OneShotFlag, fd: bun.FileDescriptor) JSC.Maybe(void) { const watcher_fd = loop.fd; - log("register: {s} ({})", .{ @tagName(flag), fd }); + log("register: FilePoll(0x{x}, generation_number={d}) {s} ({})", .{ @intFromPtr(this), this.generation_number, @tagName(flag), fd }); std.debug.assert(fd != invalid_fd); - if (one_shot) { + if (one_shot != .none) { this.flags.insert(.one_shot); } @@ -767,7 +826,13 @@ pub const FilePoll = struct { } } else if (comptime Environment.isMac) { var changelist = std.mem.zeroes([2]std.os.system.kevent64_s); - const one_shot_flag: u16 = if (!this.flags.contains(.one_shot)) 0 else std.c.EV_ONESHOT; + const one_shot_flag: u16 = if (!this.flags.contains(.one_shot)) + 0 + else if (one_shot == .dispatch) + std.c.EV_DISPATCH | std.c.EV_ENABLE + else + std.c.EV_ONESHOT; + changelist[0] = switch (flag) { .readable => .{ .ident = @intCast(fd.cast()), @@ -916,7 +981,7 @@ pub const FilePoll = struct { return JSC.Maybe(void).success; } - log("unregister: {s} ({})", .{ @tagName(flag), fd }); + log("unregister: FilePoll(0x{x}, generation_number={d}) {s} ({})", .{ @intFromPtr(this), this.generation_number, @tagName(flag), fd }); if (comptime Environment.isLinux) { const ctl = linux.epoll_ctl( @@ -1021,3 +1086,25 @@ pub const FilePoll = struct { }; pub const Waker = bun.AsyncIO.Waker; + +pub const Closer = struct { + fd: bun.FileDescriptor, + task: JSC.WorkPoolTask = .{ .callback = &onClose }, + + pub usingnamespace bun.New(@This()); + + pub fn close( + fd: bun.FileDescriptor, + /// for compatibiltiy with windows version + _: anytype, + ) void { + std.debug.assert(fd != bun.invalid_fd); + JSC.WorkPool.schedule(&Closer.new(.{ .fd = fd }).task); + } + + fn onClose(task: *JSC.WorkPoolTask) void { + const closer = @fieldParentPtr(Closer, "task", task); + defer closer.destroy(); + _ = bun.sys.close(closer.fd); + } +}; diff --git a/src/async/windows_event_loop.zig b/src/async/windows_event_loop.zig index 6888e23b7a93da..4897ea9c9a98d3 100644 --- a/src/async/windows_event_loop.zig +++ b/src/async/windows_event_loop.zig @@ -51,11 +51,15 @@ pub const KeepAlive = struct { /// Prevent a poll from keeping the process alive. pub fn unref(this: *KeepAlive, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); if (this.status != .active) return; this.status = .inactive; - event_loop_ctx.platformEventLoop().dec(); + if (comptime @TypeOf(event_loop_ctx_) == JSC.EventLoopHandle) { + event_loop_ctx_.loop().subActive(1); + return; + } + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); + event_loop_ctx.platformEventLoop().subActive(1); } /// From another thread, Prevent a poll from keeping the process alive. @@ -88,11 +92,16 @@ pub const KeepAlive = struct { /// Allow a poll to keep the process alive. pub fn ref(this: *KeepAlive, event_loop_ctx_: anytype) void { - const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); if (this.status != .inactive) return; this.status = .active; - event_loop_ctx.platformEventLoop().inc(); + const EventLoopContext = @TypeOf(event_loop_ctx_); + if (comptime EventLoopContext == JSC.EventLoopHandle) { + event_loop_ctx_.ref(); + return; + } + const event_loop_ctx = JSC.AbstractVM(event_loop_ctx_); + event_loop_ctx.platformEventLoop().ref(); } /// Allow a poll to keep the process alive. @@ -168,20 +177,6 @@ pub const FilePoll = struct { return poll; } - pub fn initWithPackageManager(m: *bun.PackageManager, fd: bun.FileDescriptor, flags: Flags.Struct, owner: anytype) *FilePoll { - return initWithPackageManagerWithOwner(m, fd, flags, Owner.init(owner)); - } - - pub fn initWithPackageManagerWithOwner(manager: *bun.PackageManager, fd: bun.FileDescriptor, flags: Flags.Struct, owner: Owner) *FilePoll { - var poll = manager.file_poll_store.get(); - poll.fd = fd; - poll.flags = Flags.Set.init(flags); - poll.owner = owner; - poll.next_to_free = null; - - return poll; - } - pub fn deinit(this: *FilePoll) void { const vm = JSC.VirtualMachine.get(); this.deinitWithVM(vm); @@ -372,19 +367,17 @@ pub const FilePoll = struct { }; pub const Waker = struct { - loop: *bun.uws.UVLoop, + loop: *bun.uws.WindowsLoop, - pub fn init(_: std.mem.Allocator) !Waker { - return .{ .loop = bun.uws.UVLoop.init() }; + pub fn init() !Waker { + return .{ .loop = bun.uws.WindowsLoop.get() }; } - pub fn getFd(this: *const Waker) bun.FileDescriptor { - _ = this; - + pub fn getFd(_: *const Waker) bun.FileDescriptor { @compileError("Waker.getFd is unsupported on Windows"); } - pub fn initWithFileDescriptor(_: std.mem.Allocator, _: bun.FileDescriptor) Waker { + pub fn initWithFileDescriptor(_: bun.FileDescriptor) Waker { @compileError("Waker.initWithFileDescriptor is unsupported on Windows"); } diff --git a/src/baby_list.zig b/src/baby_list.zig index 07a25959f40de2..babf6e2850281b 100644 --- a/src/baby_list.zig +++ b/src/baby_list.zig @@ -65,6 +65,16 @@ pub fn BabyList(comptime Type: type) type { }; } + pub fn clearRetainingCapacity(this: *@This()) void { + var list_ = this.listManaged(bun.default_allocator); + list_.clearRetainingCapacity(); + } + + pub fn replaceRange(this: *@This(), start: usize, len_: usize, new_items: []const Type) !void { + var list_ = this.listManaged(bun.default_allocator); + try list_.replaceRange(start, len_, new_items); + } + pub fn appendAssumeCapacity(this: *@This(), value: Type) void { this.ptr[this.len] = value; this.len += 1; @@ -140,6 +150,12 @@ pub fn BabyList(comptime Type: type) type { }; } + pub fn allocatedSlice(this: *const ListType) []u8 { + if (this.cap == 0) return &.{}; + + return this.ptr[0..this.cap]; + } + pub fn update(this: *ListType, list_: anytype) void { this.* = .{ .ptr = list_.items.ptr, @@ -210,6 +226,12 @@ pub fn BabyList(comptime Type: type) type { this.update(list_); } + pub fn appendFmt(this: *@This(), allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) !void { + var list__ = this.listManaged(allocator); + const writer = list__.writer(); + try writer.print(fmt, args); + } + pub fn append(this: *@This(), allocator: std.mem.Allocator, value: []const Type) !void { var list__ = this.listManaged(allocator); try list__.appendSlice(value); diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index b0d932494794e9..15903af626fd82 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -35,6 +35,8 @@ writer: BufferedWriter, counts: Counter = .{}, +pub fn format(_: @This(), comptime _: []const u8, _: anytype, _: anytype) !void {} + pub fn init(error_writer: Output.WriterType, writer: Output.WriterType) ConsoleObject { return ConsoleObject{ .error_writer = BufferedWriter{ .unbuffered_writer = error_writer }, @@ -195,7 +197,7 @@ pub fn messageWithTypeAndLevel( } if (print_length > 0) - format( + format2( level, global, vals, @@ -619,11 +621,13 @@ const TablePrinter = struct { pub fn writeTrace(comptime Writer: type, writer: Writer, global: *JSGlobalObject) void { var holder = ZigException.Holder.init(); - + var vm = VirtualMachine.get(); + defer holder.deinit(vm); const exception = holder.zigException(); + var err = ZigString.init("trace output").toErrorInstance(global); err.toZigException(global, exception); - VirtualMachine.get().remapZigException(exception, err, null); + vm.remapZigException(exception, err, null, &holder.need_to_clear_parser_arena_on_deinit); if (Output.enable_ansi_colors_stderr) VirtualMachine.printStackTrace( @@ -650,7 +654,7 @@ pub const FormatOptions = struct { max_depth: u16 = 2, }; -pub fn format( +pub fn format2( level: MessageLevel, global: *JSGlobalObject, vals: [*]const JSValue, @@ -680,7 +684,10 @@ pub fn format( const tag = ConsoleObject.Formatter.Tag.get(vals[0], global); var unbuffered_writer = if (comptime Writer != RawWriter) - writer.context.unbuffered_writer.context.writer() + if (@hasDecl(@TypeOf(writer.context.unbuffered_writer.context), "quietWriter")) + writer.context.unbuffered_writer.context.quietWriter() + else + writer.context.unbuffered_writer.context.writer() else writer; @@ -2461,7 +2468,7 @@ pub const Formatter = struct { comptime Output.prettyFmt("data: ", enable_ansi_colors), .{}, ); - const data = value.get(this.globalThis, "data").?; + const data = value.fastGet(this.globalThis, .data).?; const tag = Tag.getAdvanced(data, this.globalThis, .{ .hide_global = true }); if (tag.cell.isStringLike()) { this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index ce2a796d818f94..503b69c2809905 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -63,7 +63,7 @@ pub const Strong = struct { return .{ .globalThis = globalThis }; } - pub fn get(this: *Strong) ?JSC.JSValue { + pub fn get(this: *const Strong) ?JSC.JSValue { var ref = this.ref orelse return null; const result = ref.get(); if (result == .zero) { diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 603950bda74e32..639fab4161ddc4 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -854,7 +854,7 @@ pub fn inspect( const Writer = @TypeOf(writer); // we buffer this because it'll almost always be < 4096 // when it's under 4096, we want to avoid the dynamic allocation - ConsoleObject.format( + ConsoleObject.format2( .Debug, globalThis, @as([*]const JSValue, @ptrCast(&value)), diff --git a/src/bun.js/api/bun/dns_resolver.zig b/src/bun.js/api/bun/dns_resolver.zig index 7bf1a5a2e12236..38d655b74b648d 100644 --- a/src/bun.js/api/bun/dns_resolver.zig +++ b/src/bun.js/api/bun/dns_resolver.zig @@ -125,7 +125,7 @@ const LibInfo = struct { request.backend.libinfo.file_poll.?.registerWithFd( this.vm.event_loop_handle.?, .machport, - true, + .one_shot, bun.toFD(@intFromPtr(request.backend.libinfo.machport)), ) == .result, ); diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig new file mode 100644 index 00000000000000..4c2f86a336d4b3 --- /dev/null +++ b/src/bun.js/api/bun/process.zig @@ -0,0 +1,1748 @@ +const bun = @import("root").bun; +const std = @import("std"); +const PosixSpawn = bun.spawn; +const Environment = bun.Environment; +const JSC = bun.JSC; +const Output = bun.Output; +const uv = bun.windows.libuv; +const pid_t = if (Environment.isPosix) std.os.pid_t else uv.uv_pid_t; +const fd_t = if (Environment.isPosix) std.os.fd_t else i32; +const Maybe = JSC.Maybe; + +const win_rusage = struct { + utime: struct { + tv_sec: i64 = 0, + tv_usec: i64 = 0, + }, + stime: struct { + tv_sec: i64 = 0, + tv_usec: i64 = 0, + }, + maxrss: u64 = 0, + ixrss: u0 = 0, + idrss: u0 = 0, + isrss: u0 = 0, + minflt: u0 = 0, + majflt: u0 = 0, + nswap: u0 = 0, + inblock: u64 = 0, + oublock: u64 = 0, + msgsnd: u0 = 0, + msgrcv: u0 = 0, + nsignals: u0 = 0, + nvcsw: u0 = 0, + nivcsw: u0 = 0, +}; + +const IO_COUNTERS = extern struct { + ReadOperationCount: u64 = 0, + WriteOperationCount: u64 = 0, + OtherOperationCount: u64 = 0, + ReadTransferCount: u64 = 0, + WriteTransferCount: u64 = 0, + OtherTransferCount: u64 = 0, +}; + +extern "kernel32" fn GetProcessIoCounters(handle: std.os.windows.HANDLE, counters: *IO_COUNTERS) callconv(std.os.windows.WINAPI) c_int; + +pub fn uv_getrusage(process: *uv.uv_process_t) win_rusage { + var usage_info: Rusage = .{ .utime = .{}, .stime = .{} }; + const process_pid: *anyopaque = process.process_handle; + const WinTime = std.os.windows.FILETIME; + var starttime: WinTime = undefined; + var exittime: WinTime = undefined; + var kerneltime: WinTime = undefined; + var usertime: WinTime = undefined; + // We at least get process times + if (std.os.windows.kernel32.GetProcessTimes(process_pid, &starttime, &exittime, &kerneltime, &usertime) == 1) { + var temp: u64 = (@as(u64, kerneltime.dwHighDateTime) << 32) | kerneltime.dwLowDateTime; + if (temp > 0) { + usage_info.stime.tv_sec = @intCast(temp / 10000000); + usage_info.stime.tv_usec = @intCast(temp % 1000000); + } + temp = (@as(u64, usertime.dwHighDateTime) << 32) | usertime.dwLowDateTime; + if (temp > 0) { + usage_info.utime.tv_sec = @intCast(temp / 10000000); + usage_info.utime.tv_usec = @intCast(temp % 1000000); + } + } + var counters: IO_COUNTERS = .{}; + _ = GetProcessIoCounters(process_pid, &counters); + usage_info.inblock = counters.ReadOperationCount; + usage_info.oublock = counters.WriteOperationCount; + + const memory = std.os.windows.GetProcessMemoryInfo(process_pid) catch return usage_info; + usage_info.maxrss = memory.PeakWorkingSetSize / 1024; + + return usage_info; +} +pub const Rusage = if (Environment.isWindows) win_rusage else std.os.rusage; + +const Subprocess = JSC.Subprocess; +const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; +const ShellSubprocess = bun.shell.ShellSubprocess; +// const ShellSubprocessMini = bun.shell.ShellSubprocessMini; +pub const ProcessExitHandler = struct { + ptr: TaggedPointer = TaggedPointer.Null, + + pub const TaggedPointer = bun.TaggedPointerUnion(.{ + Subprocess, + LifecycleScriptSubprocess, + ShellSubprocess, + // ShellSubprocessMini, + }); + + pub fn init(this: *ProcessExitHandler, ptr: anytype) void { + this.ptr = TaggedPointer.init(ptr); + } + + pub fn call(this: *const ProcessExitHandler, process: *Process, status: Status, rusage: *const Rusage) void { + if (this.ptr.isNull()) { + return; + } + + switch (this.ptr.tag()) { + .Subprocess => { + const subprocess = this.ptr.as(Subprocess); + subprocess.onProcessExit(process, status, rusage); + }, + .LifecycleScriptSubprocess => { + const subprocess = this.ptr.as(LifecycleScriptSubprocess); + subprocess.onProcessExit(process, status, rusage); + }, + @field(TaggedPointer.Tag, bun.meta.typeBaseName(@typeName(ShellSubprocess))) => { + const subprocess = this.ptr.as(ShellSubprocess); + subprocess.onProcessExit(process, status, rusage); + }, + else => { + @panic("Internal Bun error: ProcessExitHandler has an invalid tag. Please file a bug report."); + }, + } + } +}; +pub const PidFDType = if (Environment.isLinux) fd_t else u0; + +pub const Process = struct { + pid: pid_t = 0, + pidfd: PidFDType = 0, + status: Status = Status{ .running = {} }, + poller: Poller = Poller{ + .detached = {}, + }, + ref_count: u32 = 1, + exit_handler: ProcessExitHandler = ProcessExitHandler{}, + sync: bool = false, + event_loop: JSC.EventLoopHandle, + + pub usingnamespace bun.NewRefCounted(Process, deinit); + + pub fn setExitHandler(this: *Process, handler: anytype) void { + this.exit_handler.init(handler); + } + + pub fn updateStatusOnWindows(this: *Process) void { + if (this.poller == .uv) { + if (!this.poller.uv.isActive() and this.status == .running) { + onExitUV(&this.poller.uv, 0, 0); + } + } + } + + pub fn initPosix( + posix: PosixSpawnResult, + event_loop: anytype, + sync: bool, + ) *Process { + return Process.new(.{ + .pid = posix.pid, + .pidfd = posix.pidfd orelse 0, + .event_loop = JSC.EventLoopHandle.init(event_loop), + .sync = sync, + .poller = .{ .detached = {} }, + }); + } + + pub fn hasExited(this: *const Process) bool { + return switch (this.status) { + .exited => true, + .signaled => true, + .err => true, + else => false, + }; + } + + pub fn hasKilled(this: *const Process) bool { + return switch (this.status) { + .exited, .signaled => true, + else => false, + }; + } + + pub fn onExit(this: *Process, status: Status, rusage: *const Rusage) void { + const exit_handler = this.exit_handler; + this.status = status; + + if (this.hasExited()) { + this.detach(); + } + + exit_handler.call(this, status, rusage); + } + + pub fn signalCode(this: *const Process) ?bun.SignalCode { + return this.status.signalCode(); + } + + pub fn waitPosix(this: *Process, sync: bool) void { + var rusage = std.mem.zeroes(Rusage); + const waitpid_result = PosixSpawn.wait4(this.pid, if (sync) 0 else std.os.W.NOHANG, &rusage); + this.onWaitPid(&waitpid_result, &rusage); + } + + pub fn wait(this: *Process, sync: bool) void { + if (comptime Environment.isPosix) { + this.waitPosix(sync); + } else if (comptime Environment.isWindows) {} + } + + pub fn onWaitPidFromWaiterThread(this: *Process, waitpid_result: *const JSC.Maybe(PosixSpawn.WaitPidResult)) void { + if (comptime Environment.isWindows) { + @compileError("not implemented on this platform"); + } + if (this.poller == .waiter_thread) { + this.poller.waiter_thread.unref(this.event_loop); + this.poller = .{ .detached = {} }; + } + this.onWaitPid(waitpid_result, &std.mem.zeroes(Rusage)); + this.deref(); + } + + pub fn onWaitPidFromEventLoopTask(this: *Process) void { + if (comptime Environment.isWindows) { + @compileError("not implemented on this platform"); + } + this.wait(false); + this.deref(); + } + + fn onWaitPid(this: *Process, waitpid_result_: *const JSC.Maybe(PosixSpawn.WaitPidResult), rusage: *const Rusage) void { + if (comptime !Environment.isPosix) { + @compileError("not implemented on this platform"); + } + + const pid = this.pid; + + var waitpid_result = waitpid_result_.*; + var rusage_result = rusage.*; + var exit_code: ?u8 = null; + var signal: ?u8 = null; + var err: ?bun.sys.Error = null; + + while (true) { + switch (waitpid_result) { + .err => |err_| { + err = err_; + }, + .result => |*result| { + if (result.pid == this.pid) { + if (std.os.W.IFEXITED(result.status)) { + exit_code = std.os.W.EXITSTATUS(result.status); + // True if the process terminated due to receipt of a signal. + } + + if (std.os.W.IFSIGNALED(result.status)) { + signal = @as(u8, @truncate(std.os.W.TERMSIG(result.status))); + } + + // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/waitpid.2.html + // True if the process has not terminated, but has stopped and can + // be restarted. This macro can be true only if the wait call spec-ified specified + // ified the WUNTRACED option or if the child process is being + // traced (see ptrace(2)). + else if (std.os.W.IFSTOPPED(result.status)) { + signal = @as(u8, @truncate(std.os.W.STOPSIG(result.status))); + } + } + }, + } + + if (exit_code == null and signal == null and err == null) { + switch (this.rewatchPosix()) { + .result => {}, + .err => |err_| { + if (comptime Environment.isMac) { + if (err_.getErrno() == .SRCH) { + waitpid_result = PosixSpawn.wait4( + pid, + if (this.sync) 0 else std.os.W.NOHANG, + &rusage_result, + ); + continue; + } + } + err = err_; + }, + } + } + + break; + } + + if (exit_code != null) { + this.onExit( + .{ + .exited = .{ .code = exit_code.?, .signal = @enumFromInt(signal orelse 0) }, + }, + &rusage_result, + ); + } else if (signal != null) { + this.onExit( + .{ + .signaled = @enumFromInt(signal.?), + }, + &rusage_result, + ); + } else if (err != null) { + this.onExit(.{ .err = err.? }, &rusage_result); + } + } + + pub fn watch(this: *Process, vm: anytype) JSC.Maybe(void) { + _ = vm; // autofix + + if (comptime Environment.isWindows) { + this.poller.uv.ref(); + return JSC.Maybe(void){ .result = {} }; + } + + if (WaiterThread.shouldUseWaiterThread()) { + this.poller = .{ .waiter_thread = .{} }; + this.poller.waiter_thread.ref(this.event_loop); + this.ref(); + WaiterThread.append(this); + return JSC.Maybe(void){ .result = {} }; + } + + const watchfd = if (comptime Environment.isLinux) this.pidfd else this.pid; + const poll = if (this.poller == .fd) + this.poller.fd + else + bun.Async.FilePoll.init(this.event_loop, bun.toFD(watchfd), .{}, Process, this); + + this.poller = .{ .fd = poll }; + this.poller.fd.enableKeepingProcessAlive(this.event_loop); + + switch (this.poller.fd.register( + this.event_loop.loop(), + .process, + true, + )) { + .result => { + this.ref(); + return JSC.Maybe(void){ .result = {} }; + }, + .err => |err| { + this.poller.fd.disableKeepingProcessAlive(this.event_loop); + + if (err.getErrno() != .SRCH) { + @panic("This shouldn't happen"); + } + + return .{ .err = err }; + }, + } + + unreachable; + } + + pub fn rewatchPosix(this: *Process) JSC.Maybe(void) { + if (WaiterThread.shouldUseWaiterThread()) { + if (this.poller != .waiter_thread) + this.poller = .{ .waiter_thread = .{} }; + this.poller.waiter_thread.ref(this.event_loop); + this.ref(); + WaiterThread.append(this); + return JSC.Maybe(void){ .result = {} }; + } + + if (this.poller == .fd) { + return this.poller.fd.register( + this.event_loop.loop(), + .process, + true, + ); + } else { + @panic("Internal Bun error: poll_ref in Subprocess is null unexpectedly. Please file a bug report."); + } + } + + fn onExitUV(process: *uv.uv_process_t, exit_status: i64, term_signal: c_int) callconv(.C) void { + const poller = @fieldParentPtr(PollerWindows, "uv", process); + var this = @fieldParentPtr(Process, "poller", poller); + const exit_code: u8 = if (exit_status >= 0) @as(u8, @truncate(@as(u64, @intCast(exit_status)))) else 0; + const signal_code: ?bun.SignalCode = if (term_signal > 0 and term_signal < @intFromEnum(bun.SignalCode.SIGSYS)) @enumFromInt(term_signal) else null; + const rusage = uv_getrusage(process); + + bun.windows.libuv.log("Process.onExit({d}) code: {d}, signal: {?}", .{ process.pid, exit_code, signal_code }); + + if (exit_code >= 0) { + this.close(); + this.onExit( + .{ + .exited = .{ .code = exit_code, .signal = signal_code orelse @enumFromInt(0) }, + }, + &rusage, + ); + } else if (signal_code) |sig| { + this.close(); + + this.onExit( + .{ .signaled = sig }, + &rusage, + ); + } else { + this.onExit( + .{ + .err = bun.sys.Error.fromCode(@intCast(exit_status), .waitpid), + }, + &rusage, + ); + } + } + + fn onCloseUV(uv_handle: *uv.uv_process_t) callconv(.C) void { + const poller = @fieldParentPtr(Poller, "uv", uv_handle); + var this = @fieldParentPtr(Process, "poller", poller); + bun.windows.libuv.log("Process.onClose({d})", .{uv_handle.pid}); + + if (this.poller == .uv) { + this.poller = .{ .detached = {} }; + } + this.deref(); + } + + pub fn close(this: *Process) void { + if (Environment.isPosix) { + switch (this.poller) { + .fd => |fd| { + fd.deinit(); + this.poller = .{ .detached = {} }; + }, + + .waiter_thread => |*waiter| { + waiter.disable(); + this.poller = .{ .detached = {} }; + }, + else => {}, + } + } else if (Environment.isWindows) { + switch (this.poller) { + .uv => |*process| { + if (comptime !Environment.isWindows) { + unreachable; + } + + if (process.isClosed()) { + this.poller = .{ .detached = {} }; + } else if (!process.isClosing()) { + this.ref(); + process.close(&onCloseUV); + } + }, + else => {}, + } + } + + if (comptime Environment.isLinux) { + if (this.pidfd != bun.invalid_fd.int() and this.pidfd > 0) { + _ = bun.sys.close(bun.toFD(this.pidfd)); + this.pidfd = @intCast(bun.invalid_fd.int()); + } + } + } + + pub fn disableKeepingEventLoopAlive(this: *Process) void { + this.poller.disableKeepingEventLoopAlive(this.event_loop); + } + + pub fn hasRef(this: *Process) bool { + return this.poller.hasRef(); + } + + pub fn enableKeepingEventLoopAlive(this: *Process) void { + if (this.hasExited()) + return; + + this.poller.enableKeepingEventLoopAlive(this.event_loop); + } + + pub fn detach(this: *Process) void { + this.close(); + this.exit_handler = .{}; + } + + fn deinit(this: *Process) void { + this.poller.deinit(); + this.destroy(); + } + + pub fn kill(this: *Process, signal: u8) Maybe(void) { + if (comptime Environment.isPosix) { + switch (this.poller) { + .waiter_thread, .fd => { + const err = std.c.kill(this.pid, signal); + if (err != 0) { + const errno_ = bun.C.getErrno(err); + + // if the process was already killed don't throw + if (errno_ != .SRCH) + return .{ .err = bun.sys.Error.fromCode(errno_, .kill) }; + } + }, + else => {}, + } + } else if (comptime Environment.isWindows) { + switch (this.poller) { + .uv => |*handle| { + if (handle.kill(signal).toError(.kill)) |err| { + // if the process was already killed don't throw + if (err.errno != @intFromEnum(bun.C.E.SRCH)) { + return .{ .err = err }; + } + } + + return .{ + .result = {}, + }; + }, + else => {}, + } + } + + return .{ + .result = {}, + }; + } +}; + +pub const Status = union(enum) { + running: void, + exited: Exited, + signaled: bun.SignalCode, + err: bun.sys.Error, + + pub const Exited = struct { + code: u8 = 0, + signal: bun.SignalCode = @enumFromInt(0), + }; + + pub fn signalCode(this: *const Status) ?bun.SignalCode { + return switch (this.*) { + .signaled => |sig| sig, + .exited => |exit| if (@intFromEnum(exit.signal) > 0) exit.signal else null, + else => null, + }; + } +}; + +pub const PollerPosix = union(enum) { + fd: *bun.Async.FilePoll, + waiter_thread: bun.Async.KeepAlive, + detached: void, + + pub fn deinit(this: *PollerPosix) void { + if (this.* == .fd) { + this.fd.deinit(); + } else if (this.* == .waiter_thread) { + this.waiter_thread.disable(); + } + } + + pub fn enableKeepingEventLoopAlive(this: *Poller, event_loop: JSC.EventLoopHandle) void { + switch (this.*) { + .fd => |poll| { + poll.enableKeepingProcessAlive(event_loop); + }, + .waiter_thread => |*waiter| { + waiter.ref(event_loop); + }, + else => {}, + } + } + + pub fn disableKeepingEventLoopAlive(this: *PollerPosix, event_loop: JSC.EventLoopHandle) void { + switch (this.*) { + .fd => |poll| { + poll.disableKeepingProcessAlive(event_loop); + }, + .waiter_thread => |*waiter| { + waiter.unref(event_loop); + }, + else => {}, + } + } + + pub fn hasRef(this: *const PollerPosix) bool { + return switch (this.*) { + .fd => this.fd.canEnableKeepingProcessAlive(), + .waiter_thread => this.waiter_thread.isActive(), + else => false, + }; + } +}; + +pub const Poller = if (Environment.isPosix) PollerPosix else PollerWindows; + +pub const PollerWindows = union(enum) { + uv: uv.uv_process_t, + detached: void, + + pub fn deinit(this: *PollerWindows) void { + if (this.* == .uv) { + std.debug.assert(this.uv.isClosed()); + } + } + + pub fn enableKeepingEventLoopAlive(this: *PollerWindows, event_loop: JSC.EventLoopHandle) void { + _ = event_loop; // autofix + switch (this.*) { + .uv => |*process| { + process.ref(); + }, + else => {}, + } + } + + pub fn disableKeepingEventLoopAlive(this: *PollerWindows, event_loop: JSC.EventLoopHandle) void { + _ = event_loop; // autofix + + // This is disabled on Windows + // uv_unref() causes the onExitUV callback to *never* be called + // This breaks a lot of stuff... + // Once fixed, re-enable "should not hang after unref" test in spawn.test + switch (this.*) { + .uv => { + this.uv.unref(); + }, + else => {}, + } + } + + pub fn hasRef(this: *const PollerWindows) bool { + return switch (this.*) { + .uv => if (Environment.isWindows) this.uv.hasRef() else unreachable, + else => false, + }; + } +}; + +pub const WaiterThread = if (Environment.isPosix) WaiterThreadPosix else struct { + pub inline fn shouldUseWaiterThread() bool { + return false; + } + + pub fn setShouldUseWaiterThread() void {} +}; + +// Machines which do not support pidfd_open (GVisor, Linux Kernel < 5.6) +// use a thread to wait for the child process to exit. +// We use a single thread to call waitpid() in a loop. +const WaiterThreadPosix = struct { + started: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + signalfd: if (Environment.isLinux) bun.FileDescriptor else u0 = undefined, + eventfd: if (Environment.isLinux) bun.FileDescriptor else u0 = undefined, + + js_process: ProcessQueue = .{}, + + pub const ProcessQueue = NewQueue(Process); + + fn NewQueue(comptime T: type) type { + return struct { + queue: ConcurrentQueue = .{}, + active: std.ArrayList(*T) = std.ArrayList(*T).init(bun.default_allocator), + + const TaskQueueEntry = struct { + process: *T, + next: ?*TaskQueueEntry = null, + + pub usingnamespace bun.New(@This()); + }; + pub const ConcurrentQueue = bun.UnboundedQueue(TaskQueueEntry, .next); + + pub const ResultTask = struct { + result: JSC.Maybe(PosixSpawn.WaitPidResult), + subprocess: *T, + + pub usingnamespace bun.New(@This()); + + pub const runFromJSThread = runFromMainThread; + + pub fn runFromMainThread(self: *@This()) void { + const result = self.result; + const subprocess = self.subprocess; + self.destroy(); + subprocess.onWaitPidFromWaiterThread(&result); + } + + pub fn runFromMainThreadMini(self: *@This(), _: *void) void { + self.runFromMainThread(); + } + }; + + pub const ResultTaskMini = struct { + result: JSC.Maybe(PosixSpawn.WaitPidResult), + subprocess: *T, + task: JSC.AnyTaskWithExtraContext = .{}, + + pub usingnamespace bun.New(@This()); + + pub const runFromJSThread = runFromMainThread; + + pub fn runFromMainThread(self: *@This()) void { + const result = self.result; + const subprocess = self.subprocess; + self.destroy(); + subprocess.onWaitPidFromWaiterThread(&result); + } + + pub fn runFromMainThreadMini(self: *@This(), _: *void) void { + self.runFromMainThread(); + } + }; + + pub fn append(self: *@This(), process: *T) void { + self.queue.push( + TaskQueueEntry.new(.{ + .process = process, + }), + ); + } + + pub fn loop(this: *@This()) void { + { + var batch = this.queue.popBatch(); + var iter = batch.iterator(); + this.active.ensureUnusedCapacity(batch.count) catch unreachable; + while (iter.next()) |task| { + this.active.appendAssumeCapacity(task.process); + task.destroy(); + } + } + + var queue: []*T = this.active.items; + var i: usize = 0; + while (queue.len > 0 and i < queue.len) { + const process = queue[i]; + const pid = process.pid; + // this case shouldn't really happen + if (pid == 0) { + _ = this.active.orderedRemove(i); + queue = this.active.items; + continue; + } + + const result = PosixSpawn.wait4(pid, std.os.W.NOHANG, null); + if (result == .err or (result == .result and result.result.pid == pid)) { + _ = this.active.orderedRemove(i); + queue = this.active.items; + + switch (process.event_loop) { + .js => |event_loop| { + event_loop.enqueueTaskConcurrent( + JSC.ConcurrentTask.create(JSC.Task.init( + ResultTask.new( + .{ + .result = result, + .subprocess = process, + }, + ), + )), + ); + }, + .mini => |mini| { + const AnyTask = JSC.AnyTaskWithExtraContext.New(ResultTaskMini, void, ResultTaskMini.runFromMainThreadMini); + const out = ResultTaskMini.new( + .{ + .result = result, + .subprocess = process, + }, + ); + out.task = AnyTask.init(out); + + mini.enqueueTaskConcurrent(&out.task); + }, + } + } + + i += 1; + } + } + }; + } + + pub fn setShouldUseWaiterThread() void { + @atomicStore(bool, &should_use_waiter_thread, true, .Monotonic); + } + + pub fn shouldUseWaiterThread() bool { + return @atomicLoad(bool, &should_use_waiter_thread, .Monotonic); + } + + pub fn append(process: anytype) void { + switch (comptime @TypeOf(process)) { + *Process => instance.js_process.append(process), + else => @compileError("Unknown Process type"), + } + + init() catch @panic("Failed to start WaiterThread"); + + if (comptime Environment.isLinux) { + const one = @as([8]u8, @bitCast(@as(usize, 1))); + _ = std.os.write(instance.eventfd.cast(), &one) catch @panic("Failed to write to eventfd"); + } + } + + var should_use_waiter_thread = false; + + const stack_size = 512 * 1024; + pub var instance: WaiterThread = .{}; + pub fn init() !void { + std.debug.assert(should_use_waiter_thread); + + if (instance.started.fetchMax(1, .Monotonic) > 0) { + return; + } + + var thread = try std.Thread.spawn(.{ .stack_size = stack_size }, loop, .{}); + thread.detach(); + + if (comptime Environment.isLinux) { + const linux = std.os.linux; + var mask = std.os.empty_sigset; + linux.sigaddset(&mask, std.os.SIG.CHLD); + instance.signalfd = bun.toFD(try std.os.signalfd(-1, &mask, linux.SFD.CLOEXEC | linux.SFD.NONBLOCK)); + instance.eventfd = bun.toFD(try std.os.eventfd(0, linux.EFD.NONBLOCK | linux.EFD.CLOEXEC | 0)); + } + } + + pub fn loop() void { + Output.Source.configureNamedThread("Waitpid"); + + var this = &instance; + + while (true) { + this.js_process.loop(); + + if (comptime Environment.isLinux) { + var polls = [_]std.os.pollfd{ + .{ + .fd = this.signalfd.cast(), + .events = std.os.POLL.IN | std.os.POLL.ERR, + .revents = 0, + }, + .{ + .fd = this.eventfd.cast(), + .events = std.os.POLL.IN | std.os.POLL.ERR, + .revents = 0, + }, + }; + + _ = std.os.poll(&polls, std.math.maxInt(i32)) catch 0; + + // Make sure we consume any pending signals + var buf: [1024]u8 = undefined; + _ = std.os.read(this.signalfd.cast(), &buf) catch 0; + } else { + var mask = std.os.empty_sigset; + var signal: c_int = std.os.SIG.CHLD; + const rc = std.c.sigwait(&mask, &signal); + _ = rc; + } + } + } +}; + +pub const PosixSpawnOptions = struct { + stdin: Stdio = .ignore, + stdout: Stdio = .ignore, + stderr: Stdio = .ignore, + extra_fds: []const Stdio = &.{}, + cwd: []const u8 = "", + detached: bool = false, + windows: void = {}, + argv0: ?[*:0]const u8 = null, + + pub const Stdio = union(enum) { + path: []const u8, + inherit: void, + ignore: void, + buffer: void, + pipe: bun.FileDescriptor, + dup2: struct { out: bun.JSC.Subprocess.StdioKind, to: bun.JSC.Subprocess.StdioKind }, + }; + + pub fn deinit(_: *const PosixSpawnOptions) void { + // no-op + } +}; + +pub const WindowsSpawnResult = struct { + process_: ?*Process = null, + stdin: StdioResult = .unavailable, + stdout: StdioResult = .unavailable, + stderr: StdioResult = .unavailable, + extra_pipes: std.ArrayList(StdioResult) = std.ArrayList(StdioResult).init(bun.default_allocator), + + pub const StdioResult = union(enum) { + /// inherit, ignore, path, pipe + unavailable: void, + + buffer: *bun.windows.libuv.Pipe, + buffer_fd: bun.FileDescriptor, + }; + + pub fn toProcess( + this: *WindowsSpawnResult, + _: anytype, + sync: bool, + ) *Process { + var process = this.process_.?; + this.process_ = null; + process.sync = sync; + return process; + } + + pub fn close(this: *WindowsSpawnResult) void { + if (this.process_) |proc| { + this.process_ = null; + proc.close(); + proc.detach(); + proc.deref(); + } + } +}; + +pub const WindowsSpawnOptions = struct { + stdin: Stdio = .ignore, + stdout: Stdio = .ignore, + stderr: Stdio = .ignore, + extra_fds: []const Stdio = &.{}, + cwd: []const u8 = "", + detached: bool = false, + windows: WindowsOptions = .{}, + argv0: ?[*:0]const u8 = null, + + pub const WindowsOptions = struct { + verbatim_arguments: bool = false, + hide_window: bool = true, + loop: JSC.EventLoopHandle = undefined, + }; + + pub const Stdio = union(enum) { + path: []const u8, + inherit: void, + ignore: void, + buffer: *bun.windows.libuv.Pipe, + pipe: bun.FileDescriptor, + dup2: struct { out: bun.JSC.Subprocess.StdioKind, to: bun.JSC.Subprocess.StdioKind }, + + pub fn deinit(this: *const Stdio) void { + if (this.* == .buffer) { + bun.default_allocator.destroy(this.buffer); + } + } + }; + + pub fn deinit(this: *const WindowsSpawnOptions) void { + this.stdin.deinit(); + this.stdout.deinit(); + this.stderr.deinit(); + for (this.extra_fds) |stdio| { + stdio.deinit(); + } + } +}; + +pub const PosixSpawnResult = struct { + pid: pid_t = 0, + pidfd: ?PidFDType = null, + stdin: ?bun.FileDescriptor = null, + stdout: ?bun.FileDescriptor = null, + stderr: ?bun.FileDescriptor = null, + extra_pipes: std.ArrayList(bun.FileDescriptor) = std.ArrayList(bun.FileDescriptor).init(bun.default_allocator), + + pub fn close(this: *WindowsSpawnResult) void { + for (this.extra_pipes.items) |fd| { + _ = bun.sys.close(fd); + } + + this.extra_pipes.clearAndFree(); + } + + pub fn toProcess( + this: *const PosixSpawnResult, + event_loop: anytype, + sync: bool, + ) *Process { + return Process.initPosix( + this.*, + event_loop, + sync, + ); + } + + fn pidfdFlagsForLinux() u32 { + const kernel = @import("../../../analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); + + // pidfd_nonblock only supported in 5.10+ + return if (kernel.orderWithoutTag(.{ .major = 5, .minor = 10, .patch = 0 }).compare(.gte)) + std.os.O.NONBLOCK + else + 0; + } + + pub fn pifdFromPid(this: *PosixSpawnResult) JSC.Maybe(PidFDType) { + if (!Environment.isLinux or WaiterThread.shouldUseWaiterThread()) { + return .{ .err = bun.sys.Error.fromCode(.NOSYS, .pidfd_open) }; + } + + var pidfd_flags = pidfdFlagsForLinux(); + + var rc = std.os.linux.pidfd_open( + @intCast(this.pid), + pidfd_flags, + ); + while (true) { + switch (std.os.linux.getErrno(rc)) { + .SUCCESS => return JSC.Maybe(PidFDType){ .result = @intCast(rc) }, + .INTR => { + rc = std.os.linux.pidfd_open( + @intCast(this.pid), + pidfd_flags, + ); + continue; + }, + else => |err| { + if (err == .INVAL) { + if (pidfd_flags != 0) { + rc = std.os.linux.pidfd_open( + @intCast(this.pid), + 0, + ); + pidfd_flags = 0; + continue; + } + } + + if (err == .NOSYS) { + WaiterThread.setShouldUseWaiterThread(); + return .{ .err = bun.sys.Error.fromCode(.NOSYS, .pidfd_open) }; + } + + var status: u32 = 0; + // ensure we don't leak the child process on error + _ = std.os.linux.wait4(this.pid, &status, 0, null); + + return .{ .err = bun.sys.Error.fromCode(err, .pidfd_open) }; + }, + } + } + + unreachable; + } +}; + +pub const SpawnOptions = if (Environment.isPosix) PosixSpawnOptions else WindowsSpawnOptions; +pub const SpawnProcessResult = if (Environment.isPosix) PosixSpawnResult else WindowsSpawnResult; +pub fn spawnProcess( + options: *const SpawnOptions, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, +) !JSC.Maybe(SpawnProcessResult) { + if (comptime Environment.isPosix) { + return spawnProcessPosix( + options, + argv, + envp, + ); + } else { + return spawnProcessWindows( + options, + argv, + envp, + ); + } +} +pub fn spawnProcessPosix( + options: *const PosixSpawnOptions, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, +) !JSC.Maybe(PosixSpawnResult) { + var actions = try PosixSpawn.Actions.init(); + defer actions.deinit(); + + var attr = try PosixSpawn.Attr.init(); + defer attr.deinit(); + + var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK; + + if (comptime Environment.isMac) { + flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; + } + + if (options.detached) { + flags |= bun.C.POSIX_SPAWN_SETSID; + } + + if (options.cwd.len > 0) { + actions.chdir(options.cwd) catch return error.ChangingDirectoryFailed; + } + var spawned = PosixSpawnResult{}; + var extra_fds = std.ArrayList(bun.FileDescriptor).init(bun.default_allocator); + errdefer extra_fds.deinit(); + var stack_fallback = std.heap.stackFallback(2048, bun.default_allocator); + const allocator = stack_fallback.get(); + var to_close_at_end = std.ArrayList(bun.FileDescriptor).init(allocator); + var to_set_cloexec = std.ArrayList(bun.FileDescriptor).init(allocator); + defer { + for (to_set_cloexec.items) |fd| { + const fcntl_flags = bun.sys.fcntl(fd, std.os.F.GETFD, 0).unwrap() catch continue; + _ = bun.sys.fcntl(fd, std.os.F.SETFD, bun.C.FD_CLOEXEC | fcntl_flags); + } + to_set_cloexec.clearAndFree(); + + for (to_close_at_end.items) |fd| { + _ = bun.sys.close(fd); + } + to_close_at_end.clearAndFree(); + } + + var to_close_on_error = std.ArrayList(bun.FileDescriptor).init(allocator); + + errdefer { + for (to_close_on_error.items) |fd| { + _ = bun.sys.close(fd); + } + } + defer to_close_on_error.clearAndFree(); + + attr.set(@intCast(flags)) catch {}; + attr.resetSignals() catch {}; + + const stdio_options: [3]PosixSpawnOptions.Stdio = .{ options.stdin, options.stdout, options.stderr }; + const stdios: [3]*?bun.FileDescriptor = .{ &spawned.stdin, &spawned.stdout, &spawned.stderr }; + + var dup_stdout_to_stderr: bool = false; + + for (0..3) |i| { + const stdio = stdios[i]; + const fileno = bun.toFD(i); + const flag = if (i == 0) @as(u32, std.os.O.RDONLY) else @as(u32, std.os.O.WRONLY); + + switch (stdio_options[i]) { + .dup2 => |dup2| { + // This is a hack to get around the ordering of the spawn actions. + // If stdout is set so that it redirects to stderr, the order of actions will be like this: + // 0. dup2(stderr, stdout) - this makes stdout point to stderr + // 1. setup stderr (will make stderr point to write end of `stderr_pipe_fds`) + // This is actually wrong, 0 will execute before 1 so stdout ends up writing to stderr instead of the pipe + // So we have to instead do `dup2(stderr_pipe_fd[1], stdout)` + // Right now we only allow one output redirection so it's okay. + if (i == 1 and dup2.to == .stderr) { + dup_stdout_to_stderr = true; + } else try actions.dup2(dup2.to.toFd(), dup2.out.toFd()); + }, + .inherit => { + try actions.inherit(fileno); + }, + .ignore => { + try actions.openZ(fileno, "/dev/null", flag | std.os.O.CREAT, 0o664); + }, + .path => |path| { + try actions.open(fileno, path, flag | std.os.O.CREAT, 0o664); + }, + .buffer => { + const fds: [2]bun.FileDescriptor = brk: { + var fds_: [2]std.c.fd_t = undefined; + const rc = std.c.socketpair(std.os.AF.UNIX, std.os.SOCK.STREAM, 0, &fds_); + if (rc != 0) { + return error.SystemResources; + } + + var before = std.c.fcntl(fds_[if (i == 0) 1 else 0], std.os.F.GETFL); + + _ = std.c.fcntl(fds_[if (i == 0) 1 else 0], std.os.F.SETFL, before | bun.C.FD_CLOEXEC); + + if (comptime Environment.isMac) { + // SO_NOSIGPIPE + before = 1; + _ = std.c.setsockopt(fds_[if (i == 0) 1 else 0], std.os.SOL.SOCKET, std.os.SO.NOSIGPIPE, &before, @sizeOf(c_int)); + } + + break :brk .{ bun.toFD(fds_[if (i == 0) 1 else 0]), bun.toFD(fds_[if (i == 0) 0 else 1]) }; + }; + + if (i == 0) { + // their copy of stdin should be readable + _ = std.c.shutdown(@intCast(fds[1].cast()), std.os.SHUT.WR); + + // our copy of stdin should be writable + _ = std.c.shutdown(@intCast(fds[0].cast()), std.os.SHUT.RD); + + if (comptime Environment.isMac) { + // macOS seems to default to around 8 KB for the buffer size + // this is comically small. + // TODO: investigate if this should be adjusted on Linux. + const so_recvbuf: c_int = 1024 * 512; + const so_sendbuf: c_int = 1024 * 512; + _ = std.c.setsockopt(fds[1].cast(), std.os.SOL.SOCKET, std.os.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int)); + _ = std.c.setsockopt(fds[0].cast(), std.os.SOL.SOCKET, std.os.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int)); + } + } else { + + // their copy of stdout or stderr should be writable + _ = std.c.shutdown(@intCast(fds[1].cast()), std.os.SHUT.RD); + + // our copy of stdout or stderr should be readable + _ = std.c.shutdown(@intCast(fds[0].cast()), std.os.SHUT.WR); + + if (comptime Environment.isMac) { + // macOS seems to default to around 8 KB for the buffer size + // this is comically small. + // TODO: investigate if this should be adjusted on Linux. + const so_recvbuf: c_int = 1024 * 512; + const so_sendbuf: c_int = 1024 * 512; + _ = std.c.setsockopt(fds[0].cast(), std.os.SOL.SOCKET, std.os.SO.RCVBUF, &so_recvbuf, @sizeOf(c_int)); + _ = std.c.setsockopt(fds[1].cast(), std.os.SOL.SOCKET, std.os.SO.SNDBUF, &so_sendbuf, @sizeOf(c_int)); + } + } + + try to_close_at_end.append(fds[1]); + try to_close_on_error.append(fds[0]); + + try actions.dup2(fds[1], fileno); + try actions.close(fds[1]); + + stdio.* = fds[0]; + }, + .pipe => |fd| { + try actions.dup2(fd, fileno); + stdio.* = fd; + }, + } + } + + if (dup_stdout_to_stderr) { + try actions.dup2(stdio_options[1].dup2.to.toFd(), stdio_options[1].dup2.out.toFd()); + } + + for (options.extra_fds, 0..) |ipc, i| { + const fileno = bun.toFD(3 + i); + + switch (ipc) { + .dup2 => @panic("TODO dup2 extra fd"), + .inherit => { + try actions.inherit(fileno); + }, + .ignore => { + try actions.openZ(fileno, "/dev/null", std.os.O.RDWR, 0o664); + }, + + .path => |path| { + try actions.open(fileno, path, std.os.O.RDWR | std.os.O.CREAT, 0o664); + }, + .buffer => { + const fds: [2]bun.FileDescriptor = brk: { + var fds_: [2]std.c.fd_t = undefined; + const rc = std.c.socketpair(std.os.AF.UNIX, std.os.SOCK.STREAM, 0, &fds_); + if (rc != 0) { + return error.SystemResources; + } + + // enable non-block + var before = std.c.fcntl(fds_[0], std.os.F.GETFL); + + _ = std.c.fcntl(fds_[0], std.os.F.SETFL, before | std.os.O.NONBLOCK | bun.C.FD_CLOEXEC); + + if (comptime Environment.isMac) { + // SO_NOSIGPIPE + _ = std.c.setsockopt(fds_[if (i == 0) 1 else 0], std.os.SOL.SOCKET, std.os.SO.NOSIGPIPE, &before, @sizeOf(c_int)); + } + + break :brk .{ bun.toFD(fds_[0]), bun.toFD(fds_[1]) }; + }; + + try to_close_at_end.append(fds[1]); + try to_close_on_error.append(fds[0]); + + try actions.dup2(fds[1], fileno); + try actions.close(fds[1]); + try extra_fds.append(fds[0]); + }, + .pipe => |fd| { + try actions.dup2(fd, fileno); + + try extra_fds.append(fd); + }, + } + } + + const argv0 = options.argv0 orelse argv[0].?; + const spawn_result = PosixSpawn.spawnZ( + argv0, + actions, + attr, + argv, + envp, + ); + + switch (spawn_result) { + .err => { + return .{ .err = spawn_result.err }; + }, + .result => |pid| { + spawned.pid = pid; + spawned.extra_pipes = extra_fds; + extra_fds = std.ArrayList(bun.FileDescriptor).init(bun.default_allocator); + + if (comptime Environment.isLinux) { + switch (spawned.pifdFromPid()) { + .result => |pidfd| { + spawned.pidfd = pidfd; + }, + .err => {}, + } + } + + return .{ .result = spawned }; + }, + } + + unreachable; +} + +pub fn spawnProcessWindows( + options: *const WindowsSpawnOptions, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, +) !JSC.Maybe(WindowsSpawnResult) { + bun.markWindowsOnly(); + + var uv_process_options = std.mem.zeroes(uv.uv_process_options_t); + + uv_process_options.args = argv; + uv_process_options.env = envp; + uv_process_options.file = options.argv0 orelse argv[0].?; + uv_process_options.exit_cb = &Process.onExitUV; + var stack_allocator = std.heap.stackFallback(8192, bun.default_allocator); + const allocator = stack_allocator.get(); + const loop = options.windows.loop.platformEventLoop().uv_loop; + + const cwd = try allocator.dupeZ(u8, options.cwd); + defer allocator.free(cwd); + + uv_process_options.cwd = cwd.ptr; + + var uv_files_to_close = std.ArrayList(uv.uv_file).init(allocator); + + var failed = false; + + defer { + for (uv_files_to_close.items) |fd| { + bun.Async.Closer.close(fd, loop); + } + uv_files_to_close.clearAndFree(); + } + + errdefer failed = true; + + if (options.windows.hide_window) { + uv_process_options.flags |= uv.UV_PROCESS_WINDOWS_HIDE; + } + + if (options.windows.verbatim_arguments) { + uv_process_options.flags |= uv.UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + } + + if (options.detached) { + uv_process_options.flags |= uv.UV_PROCESS_DETACHED; + } + + var stdio_containers = try std.ArrayList(uv.uv_stdio_container_t).initCapacity(allocator, 3 + options.extra_fds.len); + defer stdio_containers.deinit(); + @memset(stdio_containers.allocatedSlice(), std.mem.zeroes(uv.uv_stdio_container_t)); + stdio_containers.items.len = 3 + options.extra_fds.len; + + const stdios = .{ &stdio_containers.items[0], &stdio_containers.items[1], &stdio_containers.items[2] }; + const stdio_options: [3]WindowsSpawnOptions.Stdio = .{ options.stdin, options.stdout, options.stderr }; + + // On Windows it seems don't have a dup2 equivalent with pipes + // So we need to use file descriptors. + // We can create a pipe with `uv_pipe(fds, 0, 0)` and get a read fd and write fd. + // We give the write fd to stdout/stderr + // And use the read fd to read from the output. + var dup_fds: [2]uv.uv_file = .{ -1, -1 }; + var dup_src: ?u32 = null; + var dup_tgt: ?u32 = null; + inline for (0..3) |fd_i| { + const pipe_flags = uv.UV_CREATE_PIPE | uv.UV_READABLE_PIPE | uv.UV_WRITABLE_PIPE; + const stdio: *uv.uv_stdio_container_t = stdios[fd_i]; + + const flag = comptime if (fd_i == 0) @as(u32, uv.O.RDONLY) else @as(u32, uv.O.WRONLY); + + var treat_as_dup: bool = false; + + if (fd_i == 1 and stdio_options[2] == .dup2) { + treat_as_dup = true; + dup_tgt = fd_i; + } else if (fd_i == 2 and stdio_options[1] == .dup2) { + treat_as_dup = true; + dup_tgt = fd_i; + } else switch (stdio_options[fd_i]) { + .dup2 => { + treat_as_dup = true; + dup_src = fd_i; + }, + .inherit => { + stdio.flags = uv.UV_INHERIT_FD; + stdio.data.fd = fd_i; + }, + .ignore => { + stdio.flags = uv.UV_IGNORE; + }, + .path => |path| { + var req = uv.fs_t.uninitialized; + defer req.deinit(); + const rc = uv.uv_fs_open(loop, &req, &(try std.os.toPosixPath(path)), flag | uv.O.CREAT, 0o644, null); + if (rc.toError(.open)) |err| { + failed = true; + return .{ .err = err }; + } + + stdio.flags = uv.UV_INHERIT_FD; + const fd = rc.int(); + try uv_files_to_close.append(fd); + stdio.data.fd = fd; + }, + .buffer => |my_pipe| { + try my_pipe.init(loop, false).unwrap(); + stdio.flags = pipe_flags; + stdio.data.stream = @ptrCast(my_pipe); + }, + .pipe => |fd| { + stdio.flags = uv.UV_INHERIT_FD; + stdio.data.fd = bun.uvfdcast(fd); + }, + } + + if (treat_as_dup) { + if (fd_i == 1) { + if (uv.uv_pipe(&dup_fds, 0, 0).errEnum()) |e| { + return .{ .err = bun.sys.Error.fromCode(e, .pipe) }; + } + } + + stdio.flags = uv.UV_INHERIT_FD; + stdio.data = .{ .fd = dup_fds[1] }; + } + } + + for (options.extra_fds, 0..) |ipc, i| { + const stdio: *uv.uv_stdio_container_t = &stdio_containers.items[3 + i]; + + const flag = @as(u32, uv.O.RDWR); + + switch (ipc) { + .dup2 => @panic("TODO dup2 extra fd"), + .inherit => { + stdio.flags = uv.StdioFlags.inherit_fd; + stdio.data.fd = @intCast(3 + i); + }, + .ignore => { + stdio.flags = uv.UV_IGNORE; + }, + .path => |path| { + var req = uv.fs_t.uninitialized; + defer req.deinit(); + const rc = uv.uv_fs_open(loop, &req, &(try std.os.toPosixPath(path)), flag | uv.O.CREAT, 0o644, null); + if (rc.toError(.open)) |err| { + failed = true; + return .{ .err = err }; + } + + stdio.flags = uv.StdioFlags.inherit_fd; + const fd = rc.int(); + try uv_files_to_close.append(fd); + stdio.data.fd = fd; + }, + .buffer => |my_pipe| { + try my_pipe.init(loop, false).unwrap(); + stdio.flags = uv.UV_CREATE_PIPE | uv.UV_WRITABLE_PIPE | uv.UV_READABLE_PIPE | uv.UV_OVERLAPPED_PIPE; + stdio.data.stream = @ptrCast(my_pipe); + }, + .pipe => |fd| { + stdio.flags = uv.StdioFlags.inherit_fd; + stdio.data.fd = bun.uvfdcast(fd); + }, + } + } + + uv_process_options.stdio = stdio_containers.items.ptr; + uv_process_options.stdio_count = @intCast(stdio_containers.items.len); + + uv_process_options.exit_cb = &Process.onExitUV; + var process = Process.new(.{ + .event_loop = options.windows.loop, + .pid = 0, + }); + + defer { + if (failed) { + process.close(); + process.deref(); + } + } + + errdefer failed = true; + process.poller = .{ .uv = std.mem.zeroes(uv.Process) }; + + defer { + if (dup_src != null) { + if (Environment.allow_assert) std.debug.assert(dup_src != null and dup_tgt != null); + } + + if (failed) { + if (dup_fds[0] != -1) { + const r = bun.FDImpl.fromUV(dup_fds[0]).encode(); + _ = bun.sys.close(r); + } + } + + if (dup_fds[1] != -1) { + const w = bun.FDImpl.fromUV(dup_fds[1]).encode(); + _ = bun.sys.close(w); + } + } + if (process.poller.uv.spawn(loop, &uv_process_options).toError(.uv_spawn)) |err| { + failed = true; + return .{ .err = err }; + } + + process.pid = process.poller.uv.pid; + std.debug.assert(process.poller.uv.exit_cb == &Process.onExitUV); + + var result = WindowsSpawnResult{ + .process_ = process, + .extra_pipes = try std.ArrayList(WindowsSpawnResult.StdioResult).initCapacity(bun.default_allocator, options.extra_fds.len), + }; + + const result_stdios = .{ &result.stdin, &result.stdout, &result.stderr }; + inline for (0..3) |i| { + const stdio = stdio_containers.items[i]; + const result_stdio: *WindowsSpawnResult.StdioResult = result_stdios[i]; + + if (dup_src != null and i == dup_src.?) { + result_stdio.* = .unavailable; + } else if (dup_tgt != null and i == dup_tgt.?) { + result_stdio.* = .{ + .buffer_fd = bun.FDImpl.fromUV(dup_fds[0]).encode(), + }; + } else switch (stdio_options[i]) { + .buffer => { + result_stdio.* = .{ .buffer = @ptrCast(stdio.data.stream) }; + }, + else => { + result_stdio.* = .unavailable; + }, + } + } + + for (options.extra_fds, 0..) |*input, i| { + switch (input.*) { + .buffer => { + result.extra_pipes.appendAssumeCapacity(.{ .buffer = @ptrCast(stdio_containers.items[3 + i].data.stream) }); + }, + else => { + result.extra_pipes.appendAssumeCapacity(.{ .unavailable = {} }); + }, + } + } + + return .{ .result = result }; +} + +// pub const TaskProcess = struct { +// process: *Process, +// pending_error: ?bun.sys.Error = null, +// std: union(enum) { +// buffer: struct { +// out: BufferedOutput = BufferedOutput{}, +// err: BufferedOutput = BufferedOutput{}, +// }, +// unavailable: void, + +// pub fn out(this: *@This()) [2]TaskOptions.Output.Result { +// return switch (this.*) { +// .unavailable => .{ .{ .unavailable = {} }, .{ .unavailable = {} } }, +// .buffer => |*buffer| { +// return .{ +// .{ +// .buffer = buffer.out.buffer.moveToUnmanaged().items, +// }, +// .{ +// .buffer = buffer.err.buffer.moveToUnmanaged().items, +// }, +// }; +// }, +// }; +// } +// } = .{ .buffer = .{} }, +// callback: Callback = Callback{}, + +// pub const Callback = struct { +// ctx: *anyopaque = undefined, +// callback: *const fn (*anyopaque, status: Status, stdout: TaskOptions.Output.Result, stderr: TaskOptions.Output.Result) void = undefined, +// }; + +// pub inline fn loop(this: *const TaskProcess) JSC.EventLoopHandle { +// return this.process.event_loop; +// } + +// fn onReaderDone(this: *TaskProcess) void { +// this.maybeFinish(); +// } + +// fn onReaderError(this: *TaskProcess, err: bun.sys.Error) void { +// this.pending_error = err; + +// this.maybeFinish(); +// } + +// pub fn isDone(this: *const TaskProcess) bool { +// if (!this.process.hasExited()) { +// return false; +// } + +// switch (this.std) { +// .buffer => |*buffer| { +// if (!buffer.err.is_done) +// return false; + +// if (!buffer.out.is_done) +// return false; +// }, +// else => {}, +// } + +// return true; +// } + +// fn maybeFinish(this: *TaskProcess) void { +// if (!this.isDone()) { +// return; +// } + +// const status = brk: { +// if (this.pending_error) |pending_er| { +// if (this.process.status == .exited) { +// break :brk .{ .err = pending_er }; +// } +// } + +// break :brk this.process.status; +// }; + +// const callback = this.callback; +// const out, const err = this.std.out(); + +// this.process.detach(); +// this.process.deref(); +// this.deinit(); +// callback.callback(callback.ctx, status, out, err); +// } + +// pub const BufferedOutput = struct { +// poll: *bun.Async.FilePoll = undefined, +// buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), +// is_done: bool = false, + +// // This is a workaround for "Dependency loop detected" +// parent: *TaskProcess = undefined, + +// pub usingnamespace bun.io.PipeReader( +// @This(), +// getFd, +// getBuffer, +// null, +// registerPoll, +// done, +// onError, +// ); + +// pub fn getFd(this: *BufferedOutput) bun.FileDescriptor { +// return this.poll.fd; +// } + +// pub fn getBuffer(this: *BufferedOutput) *std.ArrayList(u8) { +// return &this.buffer; +// } + +// fn finish(this: *BufferedOutput) void { +// this.poll.flags.insert(.ignore_updates); +// this.parent.loop().putFilePoll(this.parent, this.poll); +// std.debug.assert(!this.is_done); +// this.is_done = true; +// } + +// pub fn done(this: *BufferedOutput, _: []u8) void { +// this.finish(); +// onReaderDone(this.parent); +// } + +// pub fn onError(this: *BufferedOutput, err: bun.sys.Error) void { +// this.finish(); +// onReaderError(this.parent, err); +// } + +// pub fn registerPoll(this: *BufferedOutput) void { +// switch (this.poll.register(this.parent().loop(), .readable, true)) { +// .err => |err| { +// this.onError(err); +// }, +// .result => {}, +// } +// } + +// pub fn start(this: *BufferedOutput) JSC.Maybe(void) { +// const maybe = this.poll.register(this.parent.loop(), .readable, true); +// if (maybe != .result) { +// this.is_done = true; +// return maybe; +// } + +// this.read(); + +// return .{ +// .result = {}, +// }; +// } +// }; + +// pub const Result = union(enum) { +// fd: bun.FileDescriptor, +// buffer: []u8, +// unavailable: void, + +// pub fn deinit(this: *const Result) void { +// return switch (this.*) { +// .fd => { +// _ = bun.sys.close(this.fd); +// }, +// .buffer => { +// bun.default_allocator.free(this.buffer); +// }, +// .unavailable => {}, +// }; +// } +// }; +// }; diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 1d2fbc935ba684..e7e3124b292275 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -266,7 +266,7 @@ const Handlers = struct { .{ "onHandshake", "handshake" }, }; inline for (pairs) |pair| { - if (opts.getTruthy(globalObject, pair.@"1")) |callback_value| { + if (opts.getTruthyComptime(globalObject, pair.@"1")) |callback_value| { if (!callback_value.isCell() or !callback_value.isCallable(globalObject.vm())) { exception.* = JSC.toInvalidArguments(comptime std.fmt.comptimePrint("Expected \"{s}\" callback to be a function", .{pair.@"1"}), .{}, globalObject).asObjectRef(); return null; @@ -450,7 +450,7 @@ pub const SocketConfig = struct { return null; }; - if (opts.getTruthy(globalObject, "data")) |default_data_value| { + if (opts.fastGet(globalObject, .data)) |default_data_value| { default_data = default_data_value; } @@ -2812,7 +2812,7 @@ fn NewSocket(comptime ssl: bool) type { } var default_data = JSValue.zero; - if (opts.getTruthy(globalObject, "data")) |default_data_value| { + if (opts.fastGet(globalObject, .data)) |default_data_value| { default_data = default_data_value; default_data.ensureStillAlive(); } diff --git a/src/bun.js/api/bun/spawn.zig b/src/bun.js/api/bun/spawn.zig index 4f9e8db7449b77..c1472a8d05420a 100644 --- a/src/bun.js/api/bun/spawn.zig +++ b/src/bun.js/api/bun/spawn.zig @@ -2,7 +2,7 @@ const JSC = @import("root").bun.JSC; const bun = @import("root").bun; const string = bun.string; const std = @import("std"); - +const Output = bun.Output; fn _getSystem() type { // this is a workaround for a Zig stage1 bug // the "usingnamespace" is evaluating in dead branches @@ -111,10 +111,7 @@ pub const BunSpawn = struct { } pub fn inherit(self: *Actions, fd: bun.FileDescriptor) !void { - _ = self; - _ = fd; - - @panic("not implemented"); + try self.dup2(fd, fd); } pub fn chdir(self: *Actions, path: []const u8) !void { @@ -316,12 +313,14 @@ pub const PosixSpawn = struct { extern fn posix_spawn_bun( pid: *c_int, + path: [*:0]const u8, request: *const BunSpawnRequest, argv: [*:null]?[*:0]const u8, envp: [*:null]?[*:0]const u8, ) isize; pub fn spawn( + path: [*:0]const u8, req_: BunSpawnRequest, argv: [*:null]?[*:0]const u8, envp: [*:null]?[*:0]const u8, @@ -329,7 +328,7 @@ pub const PosixSpawn = struct { var req = req_; var pid: c_int = 0; - const rc = posix_spawn_bun(&pid, &req, argv, envp); + const rc = posix_spawn_bun(&pid, path, &req, argv, envp); if (comptime bun.Environment.allow_assert) bun.sys.syslog("posix_spawn_bun({s}) = {d} ({d})", .{ bun.span(argv[0] orelse ""), @@ -360,6 +359,7 @@ pub const PosixSpawn = struct { ) Maybe(pid_t) { if (comptime Environment.isLinux) { return BunSpawnRequest.spawn( + path, .{ .actions = if (actions) |act| .{ .ptr = act.actions.items.ptr, @@ -412,8 +412,8 @@ pub const PosixSpawn = struct { /// See also `std.os.waitpid` for an alternative if your child process was spawned via `fork` and /// `execve` method. pub fn waitpid(pid: pid_t, flags: u32) Maybe(WaitPidResult) { - const Status = c_int; - var status: Status = 0; + const PidStatus = c_int; + var status: PidStatus = 0; while (true) { const rc = system.waitpid(pid, &status, @as(c_int, @intCast(flags))); switch (errno(rc)) { @@ -432,8 +432,8 @@ pub const PosixSpawn = struct { /// Same as waitpid, but also returns resource usage information. pub fn wait4(pid: pid_t, flags: u32, usage: ?*std.os.rusage) Maybe(WaitPidResult) { - const Status = c_int; - var status: Status = 0; + const PidStatus = c_int; + var status: PidStatus = 0; while (true) { const rc = system.wait4(pid, &status, @as(c_int, @intCast(flags)), usage); switch (errno(rc)) { @@ -449,4 +449,7 @@ pub const PosixSpawn = struct { } } } + + pub usingnamespace @import("./process.zig"); + pub usingnamespace @import("./spawn/stdio.zig"); }; diff --git a/src/bun.js/api/bun/spawn/stdio.zig b/src/bun.js/api/bun/spawn/stdio.zig new file mode 100644 index 00000000000000..8ff7b2acf978fa --- /dev/null +++ b/src/bun.js/api/bun/spawn/stdio.zig @@ -0,0 +1,481 @@ +const Allocator = std.mem.Allocator; +const uws = bun.uws; +const std = @import("std"); +const default_allocator = @import("root").bun.default_allocator; +const bun = @import("root").bun; +const Environment = bun.Environment; +const Async = bun.Async; +const JSC = @import("root").bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const Output = @import("root").bun.Output; +const os = std.os; +const uv = bun.windows.libuv; +pub const Stdio = union(enum) { + inherit: void, + capture: struct { fd: bun.FileDescriptor, buf: *bun.ByteList }, + ignore: void, + fd: bun.FileDescriptor, + dup2: struct { + out: bun.JSC.Subprocess.StdioKind, + to: bun.JSC.Subprocess.StdioKind, + }, + path: JSC.Node.PathLike, + blob: JSC.WebCore.AnyBlob, + array_buffer: JSC.ArrayBuffer.Strong, + memfd: bun.FileDescriptor, + pipe: void, + + const log = bun.sys.syslog; + + pub const Result = union(enum) { + result: bun.spawn.SpawnOptions.Stdio, + err: ToSpawnOptsError, + }; + + pub fn ResultT(comptime T: type) type { + return union(enum) { + result: T, + err: ToSpawnOptsError, + }; + } + + pub const ToSpawnOptsError = union(enum) { + stdin_used_as_out, + out_used_as_stdin, + blob_used_as_out, + uv_pipe: bun.C.E, + + pub fn toStr(this: *const @This()) []const u8 { + return switch (this.*) { + .stdin_used_as_out => "Stdin cannot be used for stdout or stderr", + .out_used_as_stdin => "Stdout and stderr cannot be used for stdin", + .blob_used_as_out => "Blobs are immutable, and cannot be used for stdout/stderr", + .uv_pipe => @panic("TODO"), + }; + } + + pub fn throwJS(this: *const @This(), globalThis: *JSC.JSGlobalObject) JSValue { + globalThis.throw("{s}", .{this.toStr()}); + return .zero; + } + }; + + pub fn byteSlice(this: *const Stdio) []const u8 { + return switch (this.*) { + .capture => this.capture.buf.slice(), + .array_buffer => this.array_buffer.array_buffer.byteSlice(), + .blob => this.blob.slice(), + else => &[_]u8{}, + }; + } + + pub fn deinit(this: *Stdio) void { + switch (this.*) { + .array_buffer => |*array_buffer| { + array_buffer.deinit(); + }, + .blob => |*blob| { + blob.detach(); + }, + .memfd => |fd| { + _ = bun.sys.close(fd); + }, + else => {}, + } + } + + pub fn canUseMemfd(this: *const @This(), is_sync: bool) bool { + if (comptime !Environment.isLinux) { + return false; + } + + return switch (this.*) { + .blob => !this.blob.needsToReadFile(), + .memfd, .array_buffer => true, + .pipe => is_sync, + else => false, + }; + } + + pub fn useMemfd(this: *@This(), index: u32) void { + if (comptime !Environment.isLinux) { + return; + } + const label = switch (index) { + 0 => "spawn_stdio_stdin", + 1 => "spawn_stdio_stdout", + 2 => "spawn_stdio_stderr", + else => "spawn_stdio_memory_file", + }; + + // We use the linux syscall api because the glibc requirement is 2.27, which is a little close for comfort. + const rc = std.os.linux.memfd_create(label, 0); + + log("memfd_create({s}) = {d}", .{ label, rc }); + + switch (std.os.linux.getErrno(rc)) { + .SUCCESS => {}, + else => |errno| { + log("Failed to create memfd: {s}", .{@tagName(errno)}); + return; + }, + } + + const fd = bun.toFD(rc); + + var remain = this.byteSlice(); + + if (remain.len > 0) + // Hint at the size of the file + _ = bun.sys.ftruncate(fd, @intCast(remain.len)); + + // Dump all the bytes in there + var written: isize = 0; + while (remain.len > 0) { + switch (bun.sys.pwrite(fd, remain, written)) { + .err => |err| { + if (err.getErrno() == .AGAIN) { + continue; + } + + Output.debugWarn("Failed to write to memfd: {s}", .{@tagName(err.getErrno())}); + _ = bun.sys.close(fd); + return; + }, + .result => |result| { + if (result == 0) { + Output.debugWarn("Failed to write to memfd: EOF", .{}); + _ = bun.sys.close(fd); + return; + } + written += @intCast(result); + remain = remain[result..]; + }, + } + } + + switch (this.*) { + .array_buffer => this.array_buffer.deinit(), + .blob => this.blob.detach(), + else => {}, + } + + this.* = .{ .memfd = fd }; + } + + fn toPosix( + stdio: *@This(), + i: u32, + ) Result { + return .{ + .result = switch (stdio.*) { + .blob => |blob| brk: { + const fd = bun.stdio(i); + if (blob.needsToReadFile()) { + if (blob.store()) |store| { + if (store.data.file.pathlike == .fd) { + if (store.data.file.pathlike.fd == fd) { + break :brk .{ .inherit = {} }; + } + + switch (bun.FDTag.get(store.data.file.pathlike.fd)) { + .stdin => { + if (i == 1 or i == 2) { + return .{ .err = .stdin_used_as_out }; + } + }, + .stdout, .stderr => { + if (i == 0) { + return .{ .err = .out_used_as_stdin }; + } + }, + else => {}, + } + + break :brk .{ .pipe = store.data.file.pathlike.fd }; + } + + break :brk .{ .path = store.data.file.pathlike.path.slice() }; + } + } + + if (i == 1 or i == 2) { + return .{ .err = .blob_used_as_out }; + } + + break :brk .{ .buffer = {} }; + }, + .dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } }, + .capture, .pipe, .array_buffer => .{ .buffer = {} }, + .fd => |fd| .{ .pipe = fd }, + .memfd => |fd| .{ .pipe = fd }, + .path => |pathlike| .{ .path = pathlike.slice() }, + .inherit => .{ .inherit = {} }, + .ignore => .{ .ignore = {} }, + }, + }; + } + + fn toWindows( + stdio: *@This(), + i: u32, + ) Result { + return .{ + .result = switch (stdio.*) { + .blob => |blob| brk: { + const fd = bun.stdio(i); + if (blob.needsToReadFile()) { + if (blob.store()) |store| { + if (store.data.file.pathlike == .fd) { + if (store.data.file.pathlike.fd == fd) { + break :brk .{ .inherit = {} }; + } + + switch (bun.FDTag.get(store.data.file.pathlike.fd)) { + .stdin => { + if (i == 1 or i == 2) { + return .{ .err = .stdin_used_as_out }; + } + }, + .stdout, .stderr => { + if (i == 0) { + return .{ .err = .out_used_as_stdin }; + } + }, + else => {}, + } + + break :brk .{ .pipe = store.data.file.pathlike.fd }; + } + + break :brk .{ .path = store.data.file.pathlike.path.slice() }; + } + } + + if (i == 1 or i == 2) { + return .{ .err = .blob_used_as_out }; + } + + break :brk .{ .buffer = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() }; + }, + .capture, .pipe, .array_buffer => .{ .buffer = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() }, + .fd => |fd| .{ .pipe = fd }, + .dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } }, + .path => |pathlike| .{ .path = pathlike.slice() }, + .inherit => .{ .inherit = {} }, + .ignore => .{ .ignore = {} }, + + .memfd => @panic("This should never happen"), + }, + }; + } + + pub fn toSync(this: *@This(), i: u32) void { + // Piping an empty stdin doesn't make sense + if (i == 0 and this.* == .pipe) { + this.* = .{ .ignore = {} }; + } + } + + /// On windows this function allocate memory ensure that .deinit() is called or ownership is passed for all *uv.Pipe + pub fn asSpawnOption( + stdio: *@This(), + i: u32, + ) Stdio.Result { + if (comptime Environment.isWindows) { + return stdio.toWindows(i); + } else { + return stdio.toPosix(i); + } + } + + pub fn isPiped(self: Stdio) bool { + return switch (self) { + .capture, .array_buffer, .blob, .pipe => true, + else => false, + }; + } + + pub fn extract( + out_stdio: *Stdio, + globalThis: *JSC.JSGlobalObject, + i: u32, + value: JSValue, + ) bool { + switch (value) { + // undefined: default + .undefined, .zero => return true, + // null: ignore + .null => { + out_stdio.* = Stdio{ .ignore = {} }; + return true; + }, + else => {}, + } + + if (value.isString()) { + const str = value.getZigString(globalThis); + if (str.eqlComptime("inherit")) { + out_stdio.* = Stdio{ .inherit = {} }; + } else if (str.eqlComptime("ignore")) { + out_stdio.* = Stdio{ .ignore = {} }; + } else if (str.eqlComptime("pipe") or str.eqlComptime("overlapped")) { + out_stdio.* = Stdio{ .pipe = {} }; + } else if (str.eqlComptime("ipc")) { + out_stdio.* = Stdio{ .pipe = {} }; // TODO: + } else { + globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', Bun.file(pathOrFd), number, or null", .{}); + return false; + } + + return true; + } else if (value.isNumber()) { + const fd = value.asFileDescriptor(); + const file_fd = bun.uvfdcast(fd); + if (file_fd < 0) { + globalThis.throwInvalidArguments("file descriptor must be a positive integer", .{}); + return false; + } + + if (file_fd >= std.math.maxInt(i32)) { + var formatter = JSC.ConsoleObject.Formatter{ .globalThis = globalThis }; + globalThis.throwInvalidArguments("file descriptor must be a valid integer, received: {}", .{ + value.toFmt(globalThis, &formatter), + }); + return false; + } + + switch (bun.FDTag.get(fd)) { + .stdin => { + if (i == 1 or i == 2) { + globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); + return false; + } + + out_stdio.* = Stdio{ .inherit = {} }; + return true; + }, + + .stdout, .stderr => |tag| { + if (i == 0) { + globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); + return false; + } + + if (i == 1 and tag == .stdout) { + out_stdio.* = .{ .inherit = {} }; + return true; + } else if (i == 2 and tag == .stderr) { + out_stdio.* = .{ .inherit = {} }; + return true; + } + }, + else => {}, + } + + out_stdio.* = Stdio{ .fd = fd }; + + return true; + } else if (value.as(JSC.WebCore.Blob)) |blob| { + return out_stdio.extractBlob(globalThis, .{ .Blob = blob.dupe() }, i); + } else if (value.as(JSC.WebCore.Request)) |req| { + req.getBodyValue().toBlobIfPossible(); + return out_stdio.extractBlob(globalThis, req.getBodyValue().useAsAnyBlob(), i); + } else if (value.as(JSC.WebCore.Response)) |req| { + req.getBodyValue().toBlobIfPossible(); + return out_stdio.extractBlob(globalThis, req.getBodyValue().useAsAnyBlob(), i); + } else if (JSC.WebCore.ReadableStream.fromJS(value, globalThis)) |req_const| { + var req = req_const; + if (i == 0) { + if (req.toAnyBlob(globalThis)) |blob| { + return out_stdio.extractBlob(globalThis, blob, i); + } + + switch (req.ptr) { + .File, .Blob => { + globalThis.throwTODO("Support fd/blob backed ReadableStream in spawn stdin. See https://github.com/oven-sh/bun/issues/8049"); + return false; + }, + .Direct, .JavaScript, .Bytes => { + // out_stdio.* = .{ .connect = req }; + globalThis.throwTODO("Re-enable ReadableStream support in spawn stdin. "); + return false; + }, + .Invalid => { + globalThis.throwInvalidArguments("ReadableStream is in invalid state.", .{}); + return false; + }, + } + } + } else if (value.asArrayBuffer(globalThis)) |array_buffer| { + if (array_buffer.slice().len == 0) { + globalThis.throwInvalidArguments("ArrayBuffer cannot be empty", .{}); + return false; + } + + out_stdio.* = .{ + .array_buffer = JSC.ArrayBuffer.Strong{ + .array_buffer = array_buffer, + .held = JSC.Strong.create(array_buffer.value, globalThis), + }, + }; + + return true; + } + + globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'ignore', or null", .{}); + return false; + } + + pub fn extractBlob( + stdio: *Stdio, + globalThis: *JSC.JSGlobalObject, + blob: JSC.WebCore.AnyBlob, + i: u32, + ) bool { + const fd = bun.stdio(i); + + if (blob.needsToReadFile()) { + if (blob.store()) |store| { + if (store.data.file.pathlike == .fd) { + if (store.data.file.pathlike.fd == fd) { + stdio.* = Stdio{ .inherit = {} }; + } else { + switch (bun.FDTag.get(i)) { + .stdin => { + if (i == 1 or i == 2) { + globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); + return false; + } + }, + + .stdout, .stderr => { + if (i == 0) { + globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); + return false; + } + }, + else => {}, + } + + stdio.* = Stdio{ .fd = store.data.file.pathlike.fd }; + } + + return true; + } + + stdio.* = .{ .path = store.data.file.pathlike.path }; + return true; + } + } + + if (i == 1 or i == 2) { + globalThis.throwInvalidArguments("Blobs are immutable, and cannot be used for stdout/stderr", .{}); + return false; + } + + stdio.* = .{ .blob = blob }; + return true; + } +}; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index b91ebb379ae72a..507d28f8bf4557 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -22,77 +22,20 @@ const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; const Body = JSC.WebCore.Body; const PosixSpawn = bun.posix.spawn; -const CloseCallbackHandler = JSC.WebCore.UVStreamSink.CloseCallbackHandler; - -const win_rusage = struct { - utime: struct { - tv_sec: i64 = 0, - tv_usec: i64 = 0, - }, - stime: struct { - tv_sec: i64 = 0, - tv_usec: i64 = 0, - }, - maxrss: u64 = 0, - ixrss: u0 = 0, - idrss: u0 = 0, - isrss: u0 = 0, - minflt: u0 = 0, - majflt: u0 = 0, - nswap: u0 = 0, - inblock: u64 = 0, - oublock: u64 = 0, - msgsnd: u0 = 0, - msgrcv: u0 = 0, - nsignals: u0 = 0, - nvcsw: u0 = 0, - nivcsw: u0 = 0, -}; - -const IO_COUNTERS = extern struct { - ReadOperationCount: u64 = 0, - WriteOperationCount: u64 = 0, - OtherOperationCount: u64 = 0, - ReadTransferCount: u64 = 0, - WriteTransferCount: u64 = 0, - OtherTransferCount: u64 = 0, -}; - -extern "kernel32" fn GetProcessIoCounters(handle: std.os.windows.HANDLE, counters: *IO_COUNTERS) callconv(std.os.windows.WINAPI) c_int; - -fn uv_getrusage(process: *uv.uv_process_t) win_rusage { - var usage_info: Rusage = .{ .utime = .{}, .stime = .{} }; - const process_pid: *anyopaque = process.process_handle; - const WinTime = std.os.windows.FILETIME; - var starttime: WinTime = undefined; - var exittime: WinTime = undefined; - var kerneltime: WinTime = undefined; - var usertime: WinTime = undefined; - // We at least get process times - if (std.os.windows.kernel32.GetProcessTimes(process_pid, &starttime, &exittime, &kerneltime, &usertime) == 1) { - var temp: u64 = (@as(u64, kerneltime.dwHighDateTime) << 32) | kerneltime.dwLowDateTime; - if (temp > 0) { - usage_info.stime.tv_sec = @intCast(temp / 10000000); - usage_info.stime.tv_usec = @intCast(temp % 1000000); - } - temp = (@as(u64, usertime.dwHighDateTime) << 32) | usertime.dwLowDateTime; - if (temp > 0) { - usage_info.utime.tv_sec = @intCast(temp / 10000000); - usage_info.utime.tv_usec = @intCast(temp % 1000000); +const Rusage = bun.posix.spawn.Rusage; +const Process = bun.posix.spawn.Process; +const WaiterThread = bun.posix.spawn.WaiterThread; +const Stdio = bun.spawn.Stdio; +const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor; +pub inline fn assertStdioResult(result: StdioResult) void { + if (comptime Environment.allow_assert) { + if (Environment.isPosix) { + if (result) |fd| { + std.debug.assert(fd != bun.invalid_fd); + } } } - var counters: IO_COUNTERS = .{}; - _ = GetProcessIoCounters(process_pid, &counters); - usage_info.inblock = counters.ReadOperationCount; - usage_info.oublock = counters.WriteOperationCount; - - const memory = std.os.windows.GetProcessMemoryInfo(process_pid) catch return usage_info; - usage_info.maxrss = memory.PeakWorkingSetSize / 1024; - - return usage_info; } -const Rusage = if (Environment.isWindows) win_rusage else std.os.rusage; - pub const ResourceUsage = struct { pub usingnamespace JSC.Codegen.JSResourceUsage; rusage: Rusage, @@ -188,38 +131,45 @@ pub const Subprocess = struct { const log = Output.scoped(.Subprocess, false); pub usingnamespace JSC.Codegen.JSSubprocess; const default_max_buffer_size = 1024 * 1024 * 4; + pub const StdioKind = enum { + stdin, + stdout, + stderr, + + pub fn toFd(this: @This()) bun.FileDescriptor { + return switch (this) { + .stdin => bun.STDIN_FD, + .stdout => bun.STDOUT_FD, + .stderr => bun.STDERR_FD, + }; + } - pid: if (Environment.isWindows) uv.uv_process_t else std.os.pid_t, - // on macOS, this is nothing - // on linux, it's a pidfd - pidfd: if (Environment.isLinux) std.os.fd_t else u0 = std.math.maxInt(if (Environment.isLinux) std.os.fd_t else u0), - pipes: if (Environment.isWindows) [3]uv.uv_pipe_t else u0 = if (Environment.isWindows) std.mem.zeroes([3]uv.uv_pipe_t) else 0, - closed_streams: u8 = 0, - deinit_onclose: bool = false, + pub fn toNum(this: @This()) c_int { + return switch (this) { + .stdin => 0, + .stdout => 1, + .stderr => 2, + }; + } + }; + process: *Process = undefined, stdin: Writable, stdout: Readable, stderr: Readable, - poll: Poll = Poll{ .poll_ref = null }, - stdio_pipes: std.ArrayListUnmanaged(Stdio.PipeExtra) = .{}, + stdio_pipes: if (Environment.isWindows) std.ArrayListUnmanaged(StdioResult) else std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, + pid_rusage: ?Rusage = null, exit_promise: JSC.Strong = .{}, on_exit_callback: JSC.Strong = .{}, - exit_code: ?u8 = null, - signal_code: ?SignalCode = null, - waitpid_err: ?bun.sys.Error = null, - globalThis: *JSC.JSGlobalObject, observable_getters: std.enums.EnumSet(enum { stdin, stdout, stderr, + stdio, }) = .{}, - closed: std.enums.EnumSet(enum { - stdin, - stdout, - stderr, - }) = .{}, + closed: std.enums.EnumSet(StdioKind) = .{}, has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), this_jsvalue: JSC.JSValue = .zero, @@ -227,26 +177,17 @@ pub const Subprocess = struct { ipc_callback: JSC.Strong = .{}, ipc: IPC.IPCData, flags: Flags = .{}, - pid_rusage: if (Environment.isWindows) ?win_rusage else ?Rusage = null, - pub const Flags = packed struct(u3) { + weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, + + pub const Flags = packed struct { is_sync: bool = false, killed: bool = false, - waiting_for_onexit: bool = false, + has_stdin_destructor_called: bool = false, }; pub const SignalCode = bun.SignalCode; - pub const Poll = union(enum) { - poll_ref: ?*Async.FilePoll, - wait_thread: WaitThreadPoll, - }; - - pub const WaitThreadPoll = struct { - ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - poll_ref: Async.KeepAlive = .{}, - }; - pub const IPCMode = enum { none, bun, @@ -265,19 +206,17 @@ pub const Subprocess = struct { this: *Subprocess, globalObject: *JSGlobalObject, ) JSValue { - if (Environment.isWindows) { - if (this.pid_rusage == null) { - this.pid_rusage = uv_getrusage(&this.pid); - if (this.pid_rusage == null) { - return JSValue.jsUndefined(); + const pid_rusage = this.pid_rusage orelse brk: { + if (Environment.isWindows) { + if (this.process.poller == .uv) { + this.pid_rusage = PosixSpawn.uv_getrusage(&this.process.poller.uv); + break :brk this.pid_rusage.?; } } - } else { - if (this.pid_rusage == null) { - return JSValue.jsUndefined(); - } - } - const pid_rusage = this.pid_rusage.?; + + return JSValue.jsUndefined(); + }; + const resource_usage = ResourceUsage{ .rusage = pid_rusage, }; @@ -291,26 +230,19 @@ pub const Subprocess = struct { } pub fn hasExited(this: *const Subprocess) bool { - return this.exit_code != null or this.waitpid_err != null or this.signal_code != null; + return this.process.hasExited(); } pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { - if (this.flags.waiting_for_onexit) { + if (this.ipc_mode != .none) { return true; } - if (this.ipc_mode != .none) { + if (this.hasPendingActivityStdio()) { return true; } - if (this.poll == .poll_ref) { - if (this.poll.poll_ref) |poll| { - if (poll.isActive() or poll.isRegistered()) { - return true; - } - } - } - if (this.poll == .wait_thread and this.poll.wait_thread.ref_count.load(.Monotonic) > 0) { + if (!this.process.hasExited()) { return true; } @@ -331,22 +263,62 @@ pub const Subprocess = struct { ); } + pub fn hasPendingActivityStdio(this: *const Subprocess) bool { + if (this.stdin.hasPendingActivity()) { + return true; + } + + inline for (.{ StdioKind.stdout, StdioKind.stderr }) |kind| { + if (@field(this, @tagName(kind)).hasPendingActivity()) { + return true; + } + } + + return false; + } + + pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void { + switch (kind) { + .stdin => { + switch (this.stdin) { + .pipe => |pipe| { + pipe.signal.clear(); + pipe.deref(); + this.stdin = .{ .ignore = {} }; + }, + .buffer => { + this.stdin.buffer.source.detach(); + this.stdin.buffer.deref(); + this.stdin = .{ .ignore = {} }; + }, + else => {}, + } + }, + inline .stdout, .stderr => |tag| { + const out: *Readable = &@field(this, @tagName(tag)); + switch (out.*) { + .pipe => |pipe| { + if (pipe.state == .done) { + out.* = .{ .buffer = pipe.state.done }; + pipe.state = .{ .done = &.{} }; + } else { + out.* = .{ .ignore = {} }; + } + pipe.deref(); + }, + else => {}, + } + }, + } + } + pub fn hasPendingActivity(this: *Subprocess) callconv(.C) bool { @fence(.Acquire); return this.has_pending_activity.load(.Acquire); } pub fn ref(this: *Subprocess) void { - const vm = this.globalThis.bunVM(); - - switch (this.poll) { - .poll_ref => if (this.poll.poll_ref) |poll| { - poll.ref(vm); - }, - .wait_thread => |*wait_thread| { - wait_thread.poll_ref.ref(vm); - }, - } + this.process.enableKeepingEventLoopAlive(); if (!this.hasCalledGetter(.stdin)) { this.stdin.ref(); @@ -357,26 +329,16 @@ pub const Subprocess = struct { } if (!this.hasCalledGetter(.stderr)) { - this.stdout.ref(); + this.stderr.ref(); } + + this.updateHasPendingActivity(); } /// This disables the keeping process alive flag on the poll and also in the stdin, stdout, and stderr - pub fn unref(this: *Subprocess, comptime deactivate_poll_ref: bool) void { - const vm = this.globalThis.bunVM(); + pub fn unref(this: *Subprocess) void { + this.process.disableKeepingEventLoopAlive(); - switch (this.poll) { - .poll_ref => if (this.poll.poll_ref) |poll| { - if (deactivate_poll_ref) { - poll.onEnded(vm); - } else { - poll.unref(vm); - } - }, - .wait_thread => |*wait_thread| { - wait_thread.poll_ref.unref(vm); - }, - } if (!this.hasCalledGetter(.stdin)) { this.stdin.unref(); } @@ -386,8 +348,10 @@ pub const Subprocess = struct { } if (!this.hasCalledGetter(.stderr)) { - this.stdout.unref(); + this.stderr.unref(); } + + this.updateHasPendingActivity(); } pub fn constructor( @@ -400,24 +364,23 @@ pub const Subprocess = struct { const Readable = union(enum) { fd: bun.FileDescriptor, memfd: bun.FileDescriptor, - - pipe: Pipe, + pipe: *PipeReader, inherit: void, ignore: void, closed: void, + buffer: []u8, + + pub fn hasPendingActivity(this: *const Readable) bool { + return switch (this.*) { + .pipe => this.pipe.hasPendingActivity(), + else => false, + }; + } pub fn ref(this: *Readable) void { switch (this.*) { .pipe => { - if (this.pipe == .buffer) { - if (Environment.isWindows) { - uv.uv_ref(@ptrCast(&this.pipe.buffer.stream)); - return; - } - if (this.pipe.buffer.stream.poll_ref) |poll| { - poll.enableKeepingProcessAlive(JSC.VirtualMachine.get()); - } - } + this.pipe.updateRef(true); }, else => {}, } @@ -426,107 +389,41 @@ pub const Subprocess = struct { pub fn unref(this: *Readable) void { switch (this.*) { .pipe => { - if (this.pipe == .buffer) { - if (Environment.isWindows) { - uv.uv_unref(@ptrCast(&this.pipe.buffer.stream)); - return; - } - if (this.pipe.buffer.stream.poll_ref) |poll| { - poll.disableKeepingProcessAlive(JSC.VirtualMachine.get()); - } - } + this.pipe.updateRef(false); }, else => {}, } } - pub const Pipe = union(enum) { - stream: JSC.WebCore.ReadableStream, - buffer: BufferedOutput, - detached: void, - - pub fn finish(this: *@This()) void { - if (this.* == .stream and this.stream.ptr == .File) { - this.stream.ptr.File.finish(); - } - } - - pub fn done(this: *@This()) void { - if (this.* == .detached) - return; - - if (this.* == .stream) { - if (this.stream.ptr == .File) this.stream.ptr.File.setSignal(JSC.WebCore.Signal{}); - this.stream.done(); - return; - } - - this.buffer.close(); - } - - pub fn toJS(this: *@This(), readable: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { - if (comptime Environment.allow_assert) - std.debug.assert(this.* != .detached); // this should be cached by the getter - - if (this.* != .stream) { - const stream = this.buffer.toReadableStream(globalThis, exited); - // we do not detach on windows - if (Environment.isWindows) { - return stream.toJS(); - } - this.* = .{ .stream = stream }; - } - - if (this.stream.ptr == .File) { - this.stream.ptr.File.setSignal(JSC.WebCore.Signal.init(readable)); - } + pub fn init(stdio: Stdio, event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: u32, is_sync: bool) Readable { + _ = allocator; // autofix + _ = max_size; // autofix + _ = is_sync; // autofix + assertStdioResult(result); - const result = this.stream.toJS(); - this.* = .detached; - return result; + if (Environment.isWindows) { + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .ignore => Readable{ .ignore = {} }, + .path => Readable{ .ignore = {} }, + .fd => |fd| Readable{ .fd = fd }, + .dup2 => |dup2| Readable{ .fd = dup2.out.toFd() }, + .memfd => Readable{ .ignore = {} }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, + .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), + .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), + }; } - }; - - pub fn initWithPipe(stdio: Stdio, pipe: *uv.uv_pipe_t, allocator: std.mem.Allocator, max_size: u32) Readable { - return switch (stdio) { - .inherit => Readable{ .inherit = {} }, - .ignore => Readable{ .ignore = {} }, - .pipe => brk: { - break :brk .{ - .pipe = .{ - .buffer = BufferedOutput.initWithPipeAndAllocator(allocator, pipe, max_size), - }, - }; - }, - .path => Readable{ .ignore = {} }, - .blob, .fd => @panic("use init() instead"), - .memfd => Readable{ .memfd = stdio.memfd }, - .array_buffer => Readable{ - .pipe = .{ - .buffer = BufferedOutput.initWithPipeAndSlice(pipe, stdio.array_buffer.slice()), - }, - }, - }; - } - pub fn init(stdio: Stdio, fd: bun.FileDescriptor, allocator: std.mem.Allocator, max_size: u32) Readable { return switch (stdio) { .inherit => Readable{ .inherit = {} }, .ignore => Readable{ .ignore = {} }, - .pipe => brk: { - break :brk .{ - .pipe = .{ - .buffer = BufferedOutput.initWithAllocator(allocator, fd, max_size), - }, - }; - }, .path => Readable{ .ignore = {} }, - .blob, .fd => Readable{ .fd = fd }, + .fd => Readable{ .fd = result.? }, .memfd => Readable{ .memfd = stdio.memfd }, - .array_buffer => Readable{ - .pipe = .{ - .buffer = BufferedOutput.initWithSlice(fd, stdio.array_buffer.slice()), - }, - }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, + .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), + .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), + .dup2 => Output.panic("TODO: implement dup2 support in Stdio readable", .{}), }; } @@ -541,53 +438,32 @@ pub const Subprocess = struct { pub fn close(this: *Readable) void { switch (this.*) { inline .memfd, .fd => |fd| { + this.* = .{ .closed = {} }; _ = bun.sys.close(fd); }, .pipe => { - this.pipe.done(); + this.pipe.close(); }, else => {}, } } - pub fn setCloseCallbackIfPossible(this: *Readable, callback: CloseCallbackHandler) bool { - switch (this.*) { - .pipe => { - if (Environment.isWindows) { - if (uv.uv_is_closed(@ptrCast(this.pipe.buffer.stream))) { - return false; - } - this.pipe.buffer.closeCallback = callback; - return true; - } - return false; - }, - else => return false, - } - } - pub fn finalize(this: *Readable) void { switch (this.*) { inline .memfd, .fd => |fd| { + this.* = .{ .closed = {} }; _ = bun.sys.close(fd); }, - .pipe => |*pipe| { - if (pipe.* == .detached) { - return; - } - - if (pipe.* == .stream and pipe.stream.ptr == .File) { - this.close(); - return; - } - - pipe.buffer.close(); + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; }, else => {}, } } pub fn toJS(this: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { + _ = exited; // autofix switch (this.*) { // should only be reachable when the entire output is buffered. .memfd => return this.toBufferedValue(globalThis), @@ -595,8 +471,20 @@ pub const Subprocess = struct { .fd => |fd| { return fd.toJS(globalThis); }, - .pipe => { - return this.pipe.toJS(this, globalThis, exited); + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; + return pipe.toJS(globalThis); + }, + .buffer => |buffer| { + defer this.* = .{ .closed = {} }; + + if (buffer.len == 0) { + return JSC.WebCore.ReadableStream.empty(globalThis); + } + + const blob = JSC.WebCore.Blob.init(buffer, bun.default_allocator, globalThis); + return JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, 0); }, else => { return JSValue.jsUndefined(); @@ -616,21 +504,15 @@ pub const Subprocess = struct { this.* = .{ .closed = {} }; return JSC.ArrayBuffer.toJSBufferFromMemfd(fd, globalThis); }, - .pipe => { - if (!Environment.isWindows) { - this.pipe.buffer.stream.close_on_empty_read = true; - this.pipe.buffer.readAll(); - } - - const bytes = this.pipe.buffer.internal_buffer.slice(); - this.pipe.buffer.internal_buffer = .{}; - - if (bytes.len > 0) { - // Return a Buffer so that they can do .toString() on it - return JSC.JSValue.createBuffer(globalThis, bytes, bun.default_allocator); - } + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; + return pipe.toBuffer(globalThis); + }, + .buffer => |buf| { + this.* = .{ .closed = {} }; - return JSC.JSValue.createBuffer(globalThis, &.{}, bun.default_allocator); + return JSC.MarkedArrayBuffer.fromBytes(buf, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); }, else => { return JSValue.jsUndefined(); @@ -644,7 +526,7 @@ pub const Subprocess = struct { globalThis: *JSGlobalObject, ) callconv(.C) JSValue { this.observable_getters.insert(.stderr); - return this.stderr.toJS(globalThis, this.exit_code != null); + return this.stderr.toJS(globalThis, this.hasExited()); } pub fn getStdin( @@ -652,7 +534,7 @@ pub const Subprocess = struct { globalThis: *JSGlobalObject, ) callconv(.C) JSValue { this.observable_getters.insert(.stdin); - return this.stdin.toJS(globalThis); + return this.stdin.toJS(globalThis, this); } pub fn getStdout( @@ -660,7 +542,7 @@ pub const Subprocess = struct { globalThis: *JSGlobalObject, ) callconv(.C) JSValue { this.observable_getters.insert(.stdout); - return this.stdout.toJS(globalThis, this.exit_code != null); + return this.stdout.toJS(globalThis, this.hasExited()); } pub fn kill( @@ -673,10 +555,16 @@ pub const Subprocess = struct { var arguments = callframe.arguments(1); // If signal is 0, then no actual signal is sent, but error checking // is still performed. - var sig: i32 = 1; + var sig: i32 = SignalCode.default; if (arguments.len > 0) { - sig = arguments.ptr[0].coerce(i32, globalThis); + if (arguments.ptr[0].isString()) { + const signal_code = arguments.ptr[0].toEnum(globalThis, "signal", SignalCode) catch return .zero; + sig = @intFromEnum(signal_code); + } else { + sig = arguments.ptr[0].coerce(i32, globalThis); + } + if (globalThis.hasException()) return .zero; } if (!(sig >= 0 and sig <= std.math.maxInt(u8))) { @@ -687,6 +575,7 @@ pub const Subprocess = struct { switch (this.tryKill(sig)) { .result => {}, .err => |err| { + // EINVAL or ENOSYS means the signal is not supported in the current platform (most likely unsupported on windows) globalThis.throwValue(err.toJSC(globalThis)); return .zero; }, @@ -696,7 +585,7 @@ pub const Subprocess = struct { } pub fn hasKilled(this: *const Subprocess) bool { - return this.exit_code != null or this.signal_code != null; + return this.process.hasKilled(); } pub fn tryKill(this: *Subprocess, sig: i32) JSC.Maybe(void) { @@ -704,60 +593,7 @@ pub const Subprocess = struct { return .{ .result = {} }; } - send_signal: { - if (comptime Environment.isLinux) { - // if these are the same, it means the pidfd is invalid. - if (!WaiterThread.shouldUseWaiterThread()) { - // should this be handled differently? - // this effectively shouldn't happen - if (this.pidfd == bun.invalid_fd.int()) { - return .{ .result = {} }; - } - - // first appeared in Linux 5.1 - const rc = std.os.linux.pidfd_send_signal(this.pidfd, @as(u8, @intCast(sig)), null, 0); - - if (rc != 0) { - const errno = std.os.linux.getErrno(rc); - - // if the process was already killed don't throw - if (errno != .SRCH and errno != .NOSYS) - return .{ .err = bun.sys.Error.fromCode(errno, .kill) }; - } else { - break :send_signal; - } - } - } - if (comptime Environment.isWindows) { - if (std.os.windows.kernel32.TerminateProcess(this.pid.process_handle, @intCast(sig)) == 0) { - const err = bun.windows.getLastErrno(); - if (comptime Environment.allow_assert) { - std.debug.assert(err != .UNKNOWN); - } - - // if the process was already killed don't throw - // - // "After a process has terminated, call to TerminateProcess with open - // handles to the process fails with ERROR_ACCESS_DENIED (5) error code." - // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess - if (err != .PERM) - return .{ .err = bun.sys.Error.fromCode(err, .kill) }; - } - - return .{ .result = {} }; - } - - const err = std.c.kill(this.pid, sig); - if (err != 0) { - const errno = bun.C.getErrno(err); - - // if the process was already killed don't throw - if (errno != .SRCH) - return .{ .err = bun.sys.Error.fromCode(errno, .kill) }; - } - } - - return .{ .result = {} }; + return this.process.kill(@intCast(sig)); } fn hasCalledGetter(this: *Subprocess, comptime getter: @Type(.EnumLiteral)) bool { @@ -768,14 +604,7 @@ pub const Subprocess = struct { if (comptime !Environment.isLinux) { return; } - - const pidfd = this.pidfd; - - this.pidfd = bun.invalid_fd.int(); - - if (pidfd != bun.invalid_fd.int()) { - _ = bun.sys.close(bun.toFD(pidfd)); - } + this.process.close(); } pub fn doRef(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { @@ -784,10 +613,17 @@ pub const Subprocess = struct { } pub fn doUnref(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { - this.unref(false); + this.unref(); return JSC.JSValue.jsUndefined(); } + pub fn onStdinDestroyed(this: *Subprocess) void { + this.flags.has_stdin_destructor_called = true; + this.weak_file_sink_stdin_ptr = null; + + this.updateHasPendingActivity(); + } + pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSValue { if (this.ipc_mode == .none) { global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); @@ -813,11 +649,15 @@ pub const Subprocess = struct { this.ipc_mode = .none; } + pub fn pid(this: *const Subprocess) i32 { + return @intCast(this.process.pid); + } + pub fn getPid( this: *Subprocess, _: *JSGlobalObject, ) callconv(.C) JSValue { - return JSValue.jsNumber(if (Environment.isWindows) this.pid.pid else this.pid); + return JSValue.jsNumber(this.pid()); } pub fn getKilled( @@ -832,779 +672,399 @@ pub const Subprocess = struct { global: *JSGlobalObject, ) callconv(.C) JSValue { const array = JSValue.createEmptyArray(global, 0); + array.push(global, .null); array.push(global, .null); // TODO: align this with options array.push(global, .null); // TODO: align this with options - array.push(global, .null); // TODO: align this with options - for (this.stdio_pipes.items) |item| { - const uno: u32 = @intCast(item.fileno); - for (0..array.getLength(global) - uno) |_| array.push(global, .null); - array.push(global, JSValue.jsNumber(item.fd)); + this.observable_getters.insert(.stdio); + var pipes = this.stdio_pipes.items; + if (this.ipc_mode != .none) { + array.push(global, .null); + pipes = pipes[@min(1, pipes.len)..]; + } + + for (pipes) |item| { + if (Environment.isWindows) { + if (item == .buffer) { + const fdno: usize = @intFromPtr(item.buffer.fd().cast()); + array.push(global, JSValue.jsNumber(fdno)); + } + } else { + array.push(global, JSValue.jsNumber(item.cast())); + } } return array; } - pub const BufferedPipeInput = struct { - remain: []const u8 = "", - input_buffer: uv.uv_buf_t = std.mem.zeroes(uv.uv_buf_t), - write_req: uv.uv_write_t = std.mem.zeroes(uv.uv_write_t), - pipe: ?*uv.uv_pipe_t, - poll_ref: ?*Async.FilePoll = null, - written: usize = 0, - deinit_onclose: bool = false, - closeCallback: CloseCallbackHandler = CloseCallbackHandler.Empty, + pub const Source = union(enum) { + blob: JSC.WebCore.AnyBlob, + array_buffer: JSC.ArrayBuffer.Strong, + detached: void, - source: union(enum) { - blob: JSC.WebCore.AnyBlob, - array_buffer: JSC.ArrayBuffer.Strong, - }, + pub fn slice(this: *const Source) []const u8 { + return switch (this.*) { + .blob => this.blob.slice(), + .array_buffer => this.array_buffer.slice(), + else => @panic("Invalid source"), + }; + } - pub fn writeIfPossible(this: *BufferedPipeInput, comptime is_sync: bool) void { - this.writeAllowBlocking(is_sync); + pub fn detach(this: *@This()) void { + switch (this.*) { + .blob => { + this.blob.detach(); + }, + .array_buffer => { + this.array_buffer.deinit(); + }, + else => {}, + } + this.* = .detached; } + }; + + pub const StaticPipeWriter = NewStaticPipeWriter(Subprocess); + + pub fn NewStaticPipeWriter(comptime ProcessType: type) type { + return struct { + writer: IOWriter = .{}, + stdio_result: StdioResult, + source: Source = .{ .detached = {} }, + process: *ProcessType = undefined, + event_loop: JSC.EventLoopHandle, + ref_count: u32 = 1, + buffer: []const u8 = "", + + pub usingnamespace bun.NewRefCounted(@This(), deinit); + const This = @This(); + const print = bun.Output.scoped(.StaticPipeWriter, false); + + pub const IOWriter = bun.io.BufferedWriter( + This, + onWrite, + onError, + onClose, + getBuffer, + flush, + ); + pub const Poll = IOWriter; - pub fn uvWriteCallback(req: *uv.uv_write_t, status: uv.ReturnCode) callconv(.C) void { - const this = bun.cast(*BufferedPipeInput, req.data); - if (this.pipe == null) return; - if (status.errEnum()) |_| { - log("uv_write({d}) fail: {d}", .{ this.remain.len, status.int() }); - this.deinit(); - return; + pub fn updateRef(this: *This, add: bool) void { + this.writer.updateRef(this.event_loop, add); } - this.written += this.remain.len; - this.remain = ""; - // we are done! - this.close(); - } + pub fn getBuffer(this: *This) []const u8 { + return this.buffer; + } - pub fn writeAllowBlocking(this: *BufferedPipeInput, allow_blocking: bool) void { - const pipe = this.pipe orelse return; + pub fn close(this: *This) void { + log("StaticPipeWriter(0x{x}) close()", .{@intFromPtr(this)}); + this.writer.close(); + } - var to_write = this.remain; + pub fn flush(this: *This) void { + if (this.buffer.len > 0) + this.writer.write(); + } - this.input_buffer = uv.uv_buf_t.init(to_write); - if (allow_blocking) { - while (true) { - if (to_write.len == 0) { - // we are done! - this.close(); - return; - } - const status = uv.uv_try_write(@ptrCast(pipe), @ptrCast(&this.input_buffer), 1); - if (status.errEnum()) |err| { - if (err == bun.C.E.AGAIN) { - //EAGAIN - this.write_req.data = this; - const write_err = uv.uv_write(&this.write_req, @ptrCast(pipe), @ptrCast(&this.input_buffer), 1, BufferedPipeInput.uvWriteCallback).int(); - if (write_err < 0) { - log("uv_write({d}) fail: {d}", .{ this.remain.len, write_err }); - this.deinit(); - } - return; - } - // fail - log("uv_try_write({d}) fail: {d}", .{ to_write.len, status.int() }); - this.deinit(); - return; - } - const bytes_written: usize = @intCast(status.int()); - this.written += bytes_written; - this.remain = this.remain[@min(bytes_written, this.remain.len)..]; - to_write = to_write[bytes_written..]; + pub fn create(event_loop: anytype, subprocess: *ProcessType, result: StdioResult, source: Source) *This { + const this = This.new(.{ + .event_loop = JSC.EventLoopHandle.init(event_loop), + .process = subprocess, + .stdio_result = result, + .source = source, + }); + if (Environment.isWindows) { + this.writer.setPipe(this.stdio_result.buffer); } - } else { - this.write_req.data = this; - const err = uv.uv_write(&this.write_req, @ptrCast(pipe), @ptrCast(&this.input_buffer), 1, BufferedPipeInput.uvWriteCallback).int(); - if (err < 0) { - log("uv_write({d}) fail: {d}", .{ this.remain.len, err }); - this.deinit(); + this.writer.setParent(this); + return this; + } + + pub fn start(this: *This) JSC.Maybe(void) { + log("StaticPipeWriter(0x{x}) start()", .{@intFromPtr(this)}); + this.ref(); + this.buffer = this.source.slice(); + if (Environment.isWindows) { + return this.writer.startWithCurrentPipe(); + } + switch (this.writer.start(this.stdio_result.?, true)) { + .err => |err| { + return .{ .err = err }; + }, + .result => { + if (comptime Environment.isPosix) { + const poll = this.writer.handle.poll; + poll.flags.insert(.socket); + } + + return .{ .result = {} }; + }, } } - } - pub fn write(this: *BufferedPipeInput) void { - this.writeAllowBlocking(false); - } + pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void { + log("StaticPipeWriter(0x{x}) onWrite(amount={d} {})", .{ @intFromPtr(this), amount, status }); + this.buffer = this.buffer[@min(amount, this.buffer.len)..]; + if (status == .end_of_file or this.buffer.len == 0) { + this.writer.close(); + } + } - fn destroy(this: *BufferedPipeInput) void { - defer this.closeCallback.run(); + pub fn onError(this: *This, err: bun.sys.Error) void { + log("StaticPipeWriter(0x{x}) onError(err={any})", .{ @intFromPtr(this), err }); + this.source.detach(); + } - this.pipe = null; - switch (this.source) { - .blob => |*blob| { - blob.detach(); - }, - .array_buffer => |*array_buffer| { - array_buffer.deinit(); - }, + pub fn onClose(this: *This) void { + log("StaticPipeWriter(0x{x}) onClose()", .{@intFromPtr(this)}); + this.source.detach(); + this.process.onCloseIO(.stdin); } - } - fn uvClosedCallback(handler: *anyopaque) callconv(.C) void { - const event = bun.cast(*uv.uv_pipe_t, handler); - var this = bun.cast(*BufferedPipeInput, event.data); - if (this.deinit_onclose) { + pub fn deinit(this: *This) void { + this.writer.end(); + this.source.detach(); this.destroy(); } - } - fn close(this: *BufferedPipeInput) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); + pub fn loop(this: *This) *uws.Loop { + return this.event_loop.loop(); } - if (this.pipe) |pipe| { - pipe.data = this; - _ = uv.uv_close(@ptrCast(pipe), BufferedPipeInput.uvClosedCallback); + pub fn watch(this: *This) void { + if (this.buffer.len > 0) { + this.writer.watch(); + } } - } - - pub fn deinit(this: *BufferedPipeInput) void { - this.deinit_onclose = true; - this.close(); - if (this.pipe == null or uv.uv_is_closed(@ptrCast(this.pipe.?))) { - this.destroy(); + pub fn eventLoop(this: *This) JSC.EventLoopHandle { + return this.event_loop; } - } - }; + }; + } - pub const BufferedInput = struct { - remain: []const u8 = "", - fd: bun.FileDescriptor = bun.invalid_fd, - poll_ref: ?*Async.FilePoll = null, - written: usize = 0, + pub const PipeReader = struct { + reader: IOReader = undefined, + process: ?*Subprocess = null, + event_loop: *JSC.EventLoop = undefined, + ref_count: u32 = 1, + state: union(enum) { + pending: void, + done: []u8, + err: bun.sys.Error, + } = .{ .pending = {} }, + stdio_result: StdioResult, - source: union(enum) { - blob: JSC.WebCore.AnyBlob, - array_buffer: JSC.ArrayBuffer.Strong, - }, + pub const IOReader = bun.io.BufferedReader; + pub const Poll = IOReader; - pub const event_loop_kind = JSC.EventLoopKind.js; + pub usingnamespace bun.NewRefCounted(PipeReader, deinit); - pub usingnamespace JSC.WebCore.NewReadyWatcher(BufferedInput, .writable, onReady); + pub fn hasPendingActivity(this: *const PipeReader) bool { + if (this.state == .pending) + return true; - pub fn onReady(this: *BufferedInput, _: i64) void { - if (this.fd == bun.invalid_fd) { - return; - } + return this.reader.hasPendingActivity(); + } - this.write(); + pub fn detach(this: *PipeReader) void { + this.process = null; + this.deref(); } - pub fn writeIfPossible(this: *BufferedInput, comptime is_sync: bool) void { - if (comptime !is_sync) { - - // we ask, "Is it possible to write right now?" - // we do this rather than epoll or kqueue() - // because we don't want to block the thread waiting for the write - switch (bun.isWritable(this.fd)) { - .ready => { - if (this.poll_ref) |poll| { - poll.flags.insert(.writable); - poll.flags.insert(.fifo); - std.debug.assert(poll.flags.contains(.poll_writable)); - } - }, - .hup => { - this.deinit(); - return; - }, - .not_ready => { - if (!this.isWatching()) this.watch(this.fd); - return; - }, - } + pub fn create(event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult) *PipeReader { + var this = PipeReader.new(.{ + .process = process, + .reader = IOReader.init(@This()), + .event_loop = event_loop, + .stdio_result = result, + }); + if (Environment.isWindows) { + this.reader.source = .{ .pipe = this.stdio_result.buffer }; } - - this.writeAllowBlocking(is_sync); + this.reader.setParent(this); + return this; } - pub fn write(this: *BufferedInput) void { - this.writeAllowBlocking(false); + pub fn readAll(this: *PipeReader) void { + if (this.state == .pending) + this.reader.read(); } - pub fn writeAllowBlocking(this: *BufferedInput, allow_blocking: bool) void { - var to_write = this.remain; - - if (to_write.len == 0) { - // we are done! - this.closeFDIfOpen(); - return; - } - - if (comptime bun.Environment.allow_assert) { - // bun.assertNonBlocking(this.fd); - } - - while (to_write.len > 0) { - switch (bun.sys.write(this.fd, to_write)) { - .err => |e| { - if (e.isRetry()) { - log("write({d}) retry", .{ - to_write.len, - }); - - this.watch(this.fd); - this.poll_ref.?.flags.insert(.fifo); - return; - } - - if (e.getErrno() == .PIPE) { - this.deinit(); - return; - } - - // fail - log("write({d}) fail: {d}", .{ to_write.len, e.errno }); - this.deinit(); - return; - }, - - .result => |bytes_written| { - this.written += bytes_written; - - log( - "write({d}) {d}", - .{ - to_write.len, - bytes_written, - }, - ); - - this.remain = this.remain[@min(bytes_written, this.remain.len)..]; - to_write = to_write[bytes_written..]; - - // we are done or it accepts no more input - if (this.remain.len == 0 or (allow_blocking and bytes_written == 0)) { - this.deinit(); - return; - } - }, - } + pub fn start(this: *PipeReader, process: *Subprocess, event_loop: *JSC.EventLoop) JSC.Maybe(void) { + this.ref(); + this.process = process; + this.event_loop = event_loop; + if (Environment.isWindows) { + return this.reader.startWithCurrentPipe(); } - } - fn closeFDIfOpen(this: *BufferedInput) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); - } + switch (this.reader.start(this.stdio_result.?, true)) { + .err => |err| { + return .{ .err = err }; + }, + .result => { + if (comptime Environment.isPosix) { + const poll = this.reader.handle.poll; + poll.flags.insert(.socket); + this.reader.flags.socket = true; + } - if (this.fd != bun.invalid_fd) { - _ = bun.sys.close(this.fd); - this.fd = bun.invalid_fd; + return .{ .result = {} }; + }, } } - pub fn deinit(this: *BufferedInput) void { - this.closeFDIfOpen(); + pub const toJS = toReadableStream; - switch (this.source) { - .blob => |*blob| { - blob.detach(); - }, - .array_buffer => |*array_buffer| { - array_buffer.deinit(); - }, + pub fn onReaderDone(this: *PipeReader) void { + const owned = this.toOwnedSlice(); + this.state = .{ .done = owned }; + if (this.process) |process| { + this.process = null; + process.onCloseIO(this.kind(process)); + this.deref(); } } - }; - pub const BufferedOutput = struct { - internal_buffer: bun.ByteList = .{}, - stream: FIFOType = undefined, - auto_sizer: ?JSC.WebCore.AutoSizer = null, - /// stream strong ref if any is available - readable_stream_ref: if (Environment.isWindows) JSC.WebCore.ReadableStream.Strong else u0 = if (Environment.isWindows) .{} else 0, - globalThis: if (Environment.isWindows) ?*JSC.JSGlobalObject else u0 = if (Environment.isWindows) null else 0, - status: Status = .{ - .pending = {}, - }, - closeCallback: CloseCallbackHandler = CloseCallbackHandler.Empty, - - const FIFOType = if (Environment.isWindows) *uv.uv_pipe_t else JSC.WebCore.FIFO; - pub const Status = union(enum) { - pending: void, - done: void, - err: bun.sys.Error, - }; - - pub fn init(fd: bun.FileDescriptor) BufferedOutput { - if (Environment.isWindows) { - @compileError("Cannot use BufferedOutput with fd on Windows please use .initWithPipe"); + pub fn kind(reader: *const PipeReader, process: *const Subprocess) StdioKind { + if (process.stdout == .pipe and process.stdout.pipe == reader) { + return .stdout; } - return BufferedOutput{ - .internal_buffer = .{}, - .stream = JSC.WebCore.FIFO{ - .fd = fd, - }, - }; - } - pub fn initWithPipe(pipe: *uv.uv_pipe_t) BufferedOutput { - if (!Environment.isWindows) { - @compileError("uv.uv_pipe_t can only be used on Windows"); + if (process.stderr == .pipe and process.stderr.pipe == reader) { + return .stderr; } - return BufferedOutput{ .internal_buffer = .{}, .stream = pipe }; - } - pub fn initWithSlice(fd: bun.FileDescriptor, slice: []u8) BufferedOutput { - if (Environment.isWindows) { - @compileError("Cannot use BufferedOutput with fd on Windows please use .initWithPipeAndSlice"); - } - return BufferedOutput{ - // fixed capacity - .internal_buffer = bun.ByteList.initWithBuffer(slice), - .auto_sizer = null, - .stream = JSC.WebCore.FIFO{ - .fd = fd, - }, - }; + @panic("We should be either stdout or stderr"); } - pub fn initWithPipeAndSlice(pipe: *uv.uv_pipe_t, slice: []u8) BufferedOutput { - if (!Environment.isWindows) { - @compileError("uv.uv_pipe_t can only be used on Window"); + pub fn toOwnedSlice(this: *PipeReader) []u8 { + if (this.state == .done) { + return this.state.done; } - return BufferedOutput{ - // fixed capacity - .internal_buffer = bun.ByteList.initWithBuffer(slice), - .auto_sizer = null, - .stream = pipe, - }; + // we do not use .toOwnedSlice() because we don't want to reallocate memory. + const out = this.reader._buffer; + this.reader._buffer.items = &.{}; + this.reader._buffer.capacity = 0; + return out.items; } - pub fn initWithAllocator(allocator: std.mem.Allocator, fd: bun.FileDescriptor, max_size: u32) BufferedOutput { - if (Environment.isWindows) { - @compileError("Cannot use BufferedOutput with fd on Windows please use .initWithPipeAndAllocator"); - } - var this = init(fd); - this.auto_sizer = .{ - .max = max_size, - .allocator = allocator, - .buffer = &this.internal_buffer, - }; - return this; + pub fn updateRef(this: *PipeReader, add: bool) void { + this.reader.updateRef(add); } - pub fn initWithPipeAndAllocator(allocator: std.mem.Allocator, pipe: *uv.uv_pipe_t, max_size: u32) BufferedOutput { - if (!Environment.isWindows) { - @compileError("uv.uv_pipe_t can only be used on Window"); - } - var this = initWithPipe(pipe); - this.auto_sizer = .{ - .max = max_size, - .allocator = allocator, - .buffer = &this.internal_buffer, - }; - return this; + pub fn watch(this: *PipeReader) void { + if (!this.reader.isDone()) + this.reader.watch(); } - pub fn onRead(this: *BufferedOutput, result: JSC.WebCore.StreamResult) void { - if (Environment.isWindows) { - @compileError("uv.uv_pipe_t can only be used on Window"); - } - switch (result) { - .pending => { - this.watch(); - return; - }, - .err => |err| { - if (err == .Error) { - this.status = .{ .err = err.Error }; - } else { - this.status = .{ .err = bun.sys.Error.fromCode(.CANCELED, .read) }; - } - this.stream.close(); + pub fn toReadableStream(this: *PipeReader, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + defer this.detach(); - return; + switch (this.state) { + .pending => { + const stream = JSC.WebCore.ReadableStream.fromPipe(globalObject, this, &this.reader); + this.state = .{ .done = &.{} }; + return stream; }, - .done => { - this.status = .{ .done = {} }; - this.stream.close(); - return; + .done => |bytes| { + const blob = JSC.WebCore.Blob.init(bytes, bun.default_allocator, globalObject); + this.state = .{ .done = &.{} }; + return JSC.WebCore.ReadableStream.fromBlob(globalObject, &blob, 0); }, - else => { - const slice = result.slice(); - this.internal_buffer.len += @as(u32, @truncate(slice.len)); - if (slice.len > 0) - std.debug.assert(this.internal_buffer.contains(slice)); - - if (result.isDone() or (slice.len == 0 and this.stream.poll_ref != null and this.stream.poll_ref.?.isHUP())) { - this.status = .{ .done = {} }; - this.stream.close(); - } + .err => |err| { + _ = err; // autofix + const empty = JSC.WebCore.ReadableStream.empty(globalObject); + JSC.WebCore.ReadableStream.cancel(&JSC.WebCore.ReadableStream.fromJS(empty, globalObject).?, globalObject); + return empty; }, } } - fn uvStreamReadCallback(handle: *uv.uv_handle_t, nread: isize, buffer: *const uv.uv_buf_t) callconv(.C) void { - const this: *BufferedOutput = @ptrCast(@alignCast(handle.data)); - if (nread <= 0) { - switch (nread) { - 0 => { - // EAGAIN or EWOULDBLOCK - return; - }, - uv.UV_EOF => { - this.status = .{ .done = {} }; - _ = uv.uv_read_stop(@ptrCast(handle)); - this.flushBufferedDataIntoReadableStream(); - }, - else => { - const rt = uv.ReturnCodeI64{ - .value = @intCast(nread), - }; - const err = rt.errEnum() orelse bun.C.E.CANCELED; - this.status = .{ .err = bun.sys.Error.fromCode(err, .read) }; - _ = uv.uv_read_stop(@ptrCast(handle)); - this.signalStreamError(); - }, - } - - // when nread < 0 buffer maybe not point to a valid address - return; - } - - this.internal_buffer.len += @as(u32, @truncate(buffer.len)); - this.flushBufferedDataIntoReadableStream(); - } - - fn uvStreamAllocCallback(handle: *uv.uv_handle_t, suggested_size: usize, buffer: *uv.uv_buf_t) callconv(.C) void { - const this: *BufferedOutput = @ptrCast(@alignCast(handle.data)); - var size: usize = 0; - var available = this.internal_buffer.available(); - if (this.auto_sizer) |auto_sizer| { - size = auto_sizer.max - this.internal_buffer.len; - if (size > suggested_size) { - size = suggested_size; - } - - if (available.len < size and this.internal_buffer.len < auto_sizer.max) { - this.internal_buffer.ensureUnusedCapacity(auto_sizer.allocator, size) catch bun.outOfMemory(); - available = this.internal_buffer.available(); - } - } else { - size = available.len; - if (size > suggested_size) { - size = suggested_size; - } - } - buffer.* = .{ .base = @ptrCast(available.ptr), .len = @intCast(size) }; - if (size == 0) { - _ = uv.uv_read_stop(@ptrCast(@alignCast(handle))); - this.status = .{ .done = {} }; - } - } - - pub fn readAll(this: *BufferedOutput) void { - if (Environment.isWindows) { - if (this.status == .pending) { - this.stream.data = this; - _ = uv.uv_read_start(@ptrCast(this.stream), BufferedOutput.uvStreamAllocCallback, BufferedOutput.uvStreamReadCallback); - } - return; - } - if (this.auto_sizer) |auto_sizer| { - while (@as(usize, this.internal_buffer.len) < auto_sizer.max and this.status == .pending) { - var stack_buffer: [8192]u8 = undefined; - const stack_buf: []u8 = stack_buffer[0..]; - var buf_to_use = stack_buf; - const available = this.internal_buffer.available(); - if (available.len >= stack_buf.len) { - buf_to_use = available; - } - - const result = this.stream.read(buf_to_use, this.stream.to_read); - - switch (result) { - .pending => { - this.watch(); - return; - }, - .err => |err| { - this.status = .{ .err = err }; - this.stream.close(); - - return; - }, - .done => { - this.status = .{ .done = {} }; - this.stream.close(); - return; - }, - .read => |slice| { - if (slice.ptr == stack_buf.ptr) { - this.internal_buffer.append(auto_sizer.allocator, slice) catch @panic("out of memory"); - } else { - this.internal_buffer.len += @as(u32, @truncate(slice.len)); - } - - if (slice.len < buf_to_use.len) { - this.watch(); - return; - } - }, - } - } - } else { - while (this.internal_buffer.len < this.internal_buffer.cap and this.status == .pending) { - const buf_to_use = this.internal_buffer.available(); - - const result = this.stream.read(buf_to_use, this.stream.to_read); - - switch (result) { - .pending => { - this.watch(); - return; - }, - .err => |err| { - this.status = .{ .err = err }; - this.stream.close(); - - return; - }, - .done => { - this.status = .{ .done = {} }; - this.stream.close(); - return; - }, - .read => |slice| { - this.internal_buffer.len += @as(u32, @truncate(slice.len)); - - if (slice.len < buf_to_use.len) { - this.watch(); - return; - } - }, - } - } + pub fn toBuffer(this: *PipeReader, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + switch (this.state) { + .done => |bytes| { + defer this.state = .{ .done = &.{} }; + return JSC.MarkedArrayBuffer.fromBytes(bytes, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); + }, + else => { + return JSC.JSValue.undefined; + }, } } - fn watch(this: *BufferedOutput) void { - if (Environment.isWindows) { - this.readAll(); - } else { - std.debug.assert(this.stream.fd != bun.invalid_fd); - this.stream.pending.set(BufferedOutput, this, onRead); - if (!this.stream.isWatching()) this.stream.watch(this.stream.fd); + pub fn onReaderError(this: *PipeReader, err: bun.sys.Error) void { + if (this.state == .done) { + bun.default_allocator.free(this.state.done); } - return; - } - - pub fn toBlob(this: *BufferedOutput, globalThis: *JSC.JSGlobalObject) JSC.WebCore.Blob { - const blob = JSC.WebCore.Blob.init(this.internal_buffer.slice(), bun.default_allocator, globalThis); - this.internal_buffer = bun.ByteList.init(""); - return blob; + this.state = .{ .err = err }; + if (this.process) |process| + process.onCloseIO(this.kind(process)); } - pub fn onStartStreamingRequestBodyCallback(ctx: *anyopaque) JSC.WebCore.DrainResult { - const this = bun.cast(*BufferedOutput, ctx); - this.readAll(); - const internal_buffer = this.internal_buffer; - this.internal_buffer = bun.ByteList.init(""); - - return .{ - .owned = .{ - .list = internal_buffer.listManaged(bun.default_allocator), - .size_hint = internal_buffer.len, + pub fn close(this: *PipeReader) void { + switch (this.state) { + .pending => { + this.reader.close(); }, - }; - } - - fn signalStreamError(this: *BufferedOutput) void { - if (this.status == .err) { - // if we are streaming update with error - if (this.readable_stream_ref.get()) |readable| { - if (readable.ptr == .Bytes) { - readable.ptr.Bytes.onData( - .{ - .err = .{ .Error = this.status.err }, - }, - bun.default_allocator, - ); - } - } - // after error we dont need the ref anymore - this.readable_stream_ref.deinit(); + .done => {}, + .err => {}, } } - fn flushBufferedDataIntoReadableStream(this: *BufferedOutput) void { - if (this.readable_stream_ref.get()) |readable| { - if (readable.ptr != .Bytes) return; - const internal_buffer = this.internal_buffer; - const isDone = this.status != .pending; - - if (internal_buffer.len > 0 or isDone) { - readable.ptr.Bytes.size_hint += internal_buffer.len; - if (isDone) { - readable.ptr.Bytes.onData( - .{ - .temporary_and_done = internal_buffer, - }, - bun.default_allocator, - ); - // no need to keep the ref anymore - this.readable_stream_ref.deinit(); - } else { - readable.ptr.Bytes.onData( - .{ - .temporary = internal_buffer, - }, - bun.default_allocator, - ); - } - this.internal_buffer.len = 0; - } - } + pub fn eventLoop(this: *PipeReader) *JSC.EventLoop { + return this.event_loop; } - fn onReadableStreamAvailable(ctx: *anyopaque, readable: JSC.WebCore.ReadableStream) void { - const this = bun.cast(*BufferedOutput, ctx); - if (this.globalThis) |globalThis| { - this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis) catch .{}; - } + pub fn loop(this: *PipeReader) *uws.Loop { + return this.event_loop.virtual_machine.uwsLoop(); } - fn toReadableStream(this: *BufferedOutput, globalThis: *JSC.JSGlobalObject, exited: bool) JSC.WebCore.ReadableStream { - if (Environment.isWindows) { - if (this.readable_stream_ref.get()) |readable| { - return readable; - } - } - - if (exited) { - // exited + received EOF => no more read() - const isClosed = if (Environment.isWindows) this.status != .pending else this.stream.isClosed(); - if (isClosed) { - // also no data at all - if (this.internal_buffer.len == 0) { - if (this.internal_buffer.cap > 0) { - if (this.auto_sizer) |auto_sizer| { - this.internal_buffer.deinitWithAllocator(auto_sizer.allocator); - } - } - // so we return an empty stream - return JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.empty(globalThis), - globalThis, - ).?; - } - - return JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.fromBlob( - globalThis, - &this.toBlob(globalThis), - 0, - ), - globalThis, - ).?; - } - } - - if (Environment.isWindows) { - this.globalThis = globalThis; - var body = Body.Value{ - .Locked = .{ - .size_hint = 0, - .task = this, - .global = globalThis, - .onStartStreaming = BufferedOutput.onStartStreamingRequestBodyCallback, - .onReadableStreamAvailable = BufferedOutput.onReadableStreamAvailable, - }, - }; - return JSC.WebCore.ReadableStream.fromJS(body.toReadableStream(globalThis), globalThis).?; - } - - { - const internal_buffer = this.internal_buffer; - this.internal_buffer = bun.ByteList.init(""); - - // There could still be data waiting to be read in the pipe - // so we need to create a new stream that will read from the - // pipe and then return the blob. - const result = JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.fromFIFO( - globalThis, - &this.stream, - internal_buffer, - ), - globalThis, - ).?; - this.stream.fd = bun.invalid_fd; - this.stream.poll_ref = null; - return result; + fn deinit(this: *PipeReader) void { + if (comptime Environment.isPosix) { + std.debug.assert(this.reader.isDone()); } - } - - fn uvClosedCallback(handler: *anyopaque) callconv(.C) void { - const event = bun.cast(*uv.uv_pipe_t, handler); - var this = bun.cast(*BufferedOutput, event.data); - this.readable_stream_ref.deinit(); - this.closeCallback.run(); - } - pub fn close(this: *BufferedOutput) void { - var needCallbackCall = true; - switch (this.status) { - .done => {}, - .pending => { - if (Environment.isWindows) { - needCallbackCall = false; - _ = uv.uv_read_stop(@ptrCast(&this.stream)); - if (uv.uv_is_closed(@ptrCast(&this.stream))) { - this.readable_stream_ref.deinit(); - this.closeCallback.run(); - } else { - _ = uv.uv_close(@ptrCast(&this.stream), BufferedOutput.uvClosedCallback); - } - } else { - this.stream.close(); - this.closeCallback.run(); - } - this.status = .{ .done = {} }; - }, - .err => {}, + if (comptime Environment.isWindows) { + std.debug.assert(this.reader.source == null or this.reader.source.?.isClosed()); } - if (this.internal_buffer.cap > 0) { - this.internal_buffer.listManaged(bun.default_allocator).deinit(); - this.internal_buffer = .{}; + if (this.state == .done) { + bun.default_allocator.free(this.state.done); } - if (Environment.isWindows and needCallbackCall) { - this.closeCallback.run(); - } + this.reader.deinit(); + this.destroy(); } }; - const SinkType = if (Environment.isWindows) *JSC.WebCore.UVStreamSink else *JSC.WebCore.FileSink; - const BufferedInputType = if (Environment.isWindows) BufferedPipeInput else BufferedInput; const Writable = union(enum) { - pipe: SinkType, - pipe_to_readable_stream: struct { - pipe: SinkType, - readable_stream: JSC.WebCore.ReadableStream, - }, + pipe: *JSC.WebCore.FileSink, fd: bun.FileDescriptor, - buffered_input: BufferedInputType, + buffer: *StaticPipeWriter, memfd: bun.FileDescriptor, inherit: void, ignore: void, + pub fn hasPendingActivity(this: *const Writable) bool { + return switch (this.*) { + .pipe => false, + + // we mark them as .ignore when they are closed, so this must be true + .buffer => true, + else => false, + }; + } + pub fn ref(this: *Writable) void { switch (this.*) { .pipe => { - if (Environment.isWindows) { - _ = uv.uv_ref(@ptrCast(this.pipe.stream)); - } else if (this.pipe.poll_ref) |poll| { - poll.enableKeepingProcessAlive(JSC.VirtualMachine.get()); - } + this.pipe.updateRef(true); + }, + .buffer => { + this.buffer.updateRef(true); }, else => {}, } @@ -1613,11 +1073,10 @@ pub const Subprocess = struct { pub fn unref(this: *Writable) void { switch (this.*) { .pipe => { - if (Environment.isWindows) { - _ = uv.uv_unref(@ptrCast(this.pipe.stream)); - } else if (this.pipe.poll_ref) |poll| { - poll.disableKeepingProcessAlive(JSC.VirtualMachine.get()); - } + this.pipe.updateRef(false); + }, + .buffer => { + this.buffer.updateRef(false); }, else => {}, } @@ -1626,6 +1085,26 @@ pub const Subprocess = struct { // When the stream has closed we need to be notified to prevent a use-after-free // We can test for this use-after-free by enabling hot module reloading on a file and then saving it twice pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { + const process = @fieldParentPtr(Subprocess, "stdin", this); + + if (process.this_jsvalue != .zero) { + if (Subprocess.stdinGetCached(process.this_jsvalue)) |existing_value| { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); + } + } + + switch (this.*) { + .buffer => { + this.buffer.deref(); + }, + .pipe => { + this.pipe.deref(); + }, + else => {}, + } + + process.onStdinDestroyed(); + this.* = .{ .ignore = {}, }; @@ -1633,99 +1112,106 @@ pub const Subprocess = struct { pub fn onReady(_: *Writable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} pub fn onStart(_: *Writable) void {} - pub fn initWithPipe(stdio: Stdio, pipe: *uv.uv_pipe_t, globalThis: *JSC.JSGlobalObject) !Writable { - switch (stdio) { - .pipe => |maybe_readable| { - const sink = try globalThis.bunVM().allocator.create(JSC.WebCore.UVStreamSink); - sink.* = .{ - .buffer = bun.ByteList{}, - .stream = @ptrCast(pipe), - .allocator = globalThis.bunVM().allocator, - .done = false, - .signal = .{}, - .next = null, - }; + pub fn init( + stdio: Stdio, + event_loop: *JSC.EventLoop, + subprocess: *Subprocess, + result: StdioResult, + ) !Writable { + assertStdioResult(result); + + if (Environment.isWindows) { + switch (stdio) { + .pipe => { + if (result == .buffer) { + const pipe = JSC.WebCore.FileSink.createWithPipe(event_loop, result.buffer); + + switch (pipe.writer.startWithCurrentPipe()) { + .result => {}, + .err => |err| { + _ = err; // autofix + pipe.deref(); + return error.UnexpectedCreatingStdin; + }, + } + + subprocess.weak_file_sink_stdin_ptr = pipe; + subprocess.flags.has_stdin_destructor_called = false; - if (maybe_readable) |readable| { + return Writable{ + .pipe = pipe, + }; + } + return Writable{ .inherit = {} }; + }, + + .blob => |blob| { return Writable{ - .pipe_to_readable_stream = .{ - .pipe = sink, - .readable_stream = readable, - }, + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), }; - } + }, + .array_buffer => |array_buffer| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), + }; + }, + .fd => |fd| { + return Writable{ .fd = fd }; + }, + .dup2 => |dup2| { + return Writable{ .fd = dup2.to.toFd() }; + }, + .inherit => { + return Writable{ .inherit = {} }; + }, + .memfd, .path, .ignore => { + return Writable{ .ignore = {} }; + }, + .capture => { + return Writable{ .ignore = {} }; + }, + } + } + switch (stdio) { + .dup2 => @panic("TODO dup2 stdio"), + .pipe => { + const pipe = JSC.WebCore.FileSink.create(event_loop, result.?); - return Writable{ .pipe = sink }; - }, - .array_buffer, .blob => { - var buffered_input: BufferedPipeInput = .{ .pipe = pipe, .source = undefined }; - switch (stdio) { - .array_buffer => |array_buffer| { - buffered_input.source = .{ .array_buffer = array_buffer }; - }, - .blob => |blob| { - buffered_input.source = .{ .blob = blob }; + switch (pipe.writer.start(pipe.fd, true)) { + .result => {}, + .err => |err| { + _ = err; // autofix + pipe.deref(); + return error.UnexpectedCreatingStdin; }, - else => unreachable, } - return Writable{ .buffered_input = buffered_input }; - }, - .memfd => { - return Writable{ .memfd = stdio.memfd }; - }, - .fd => |fd| { - return Writable{ .fd = fd }; - }, - .inherit => { - return Writable{ .inherit = {} }; - }, - .path, .ignore => { - return Writable{ .ignore = {} }; - }, - } - } - pub fn init(stdio: Stdio, fd: bun.FileDescriptor, globalThis: *JSC.JSGlobalObject) !Writable { - switch (stdio) { - .pipe => |maybe_readable| { - if (Environment.isWindows) @panic("TODO"); - var sink = try globalThis.bunVM().allocator.create(JSC.WebCore.FileSink); - sink.* = .{ - .fd = fd, - .buffer = bun.ByteList{}, - .allocator = globalThis.bunVM().allocator, - .auto_close = true, + + subprocess.weak_file_sink_stdin_ptr = pipe; + subprocess.flags.has_stdin_destructor_called = false; + + pipe.writer.handle.poll.flags.insert(.socket); + + return Writable{ + .pipe = pipe, }; - sink.mode = bun.S.IFIFO; - sink.watch(fd); - if (maybe_readable) |readable| { - return Writable{ - .pipe_to_readable_stream = .{ - .pipe = sink, - .readable_stream = readable, - }, - }; - } + }, - return Writable{ .pipe = sink }; + .blob => |blob| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), + }; }, - .array_buffer, .blob => { - var buffered_input: BufferedInput = .{ .fd = fd, .source = undefined }; - switch (stdio) { - .array_buffer => |array_buffer| { - buffered_input.source = .{ .array_buffer = array_buffer }; - }, - .blob => |blob| { - buffered_input.source = .{ .blob = blob }; - }, - else => unreachable, - } - return Writable{ .buffered_input = buffered_input }; + .array_buffer => |array_buffer| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), + }; }, - .memfd => { - return Writable{ .memfd = stdio.memfd }; + .memfd => |memfd| { + std.debug.assert(memfd != bun.invalid_fd); + return Writable{ .memfd = memfd }; }, .fd => { - return Writable{ .fd = fd }; + return Writable{ .fd = result.? }; }, .inherit => { return Writable{ .inherit = {} }; @@ -1733,85 +1219,75 @@ pub const Subprocess = struct { .path, .ignore => { return Writable{ .ignore = {} }; }, + .capture => { + return Writable{ .ignore = {} }; + }, } } - pub fn toJS(this: Writable, globalThis: *JSC.JSGlobalObject) JSValue { - return switch (this) { - .pipe => |pipe| pipe.toJS(globalThis), + pub fn toJS(this: *Writable, globalThis: *JSC.JSGlobalObject, subprocess: *Subprocess) JSValue { + return switch (this.*) { .fd => |fd| fd.toJS(globalThis), .memfd, .ignore => JSValue.jsUndefined(), - .inherit => JSValue.jsUndefined(), - .buffered_input => JSValue.jsUndefined(), - .pipe_to_readable_stream => this.pipe_to_readable_stream.readable_stream.value, + .buffer, .inherit => JSValue.jsUndefined(), + .pipe => |pipe| { + this.* = .{ .ignore = {} }; + if (subprocess.process.hasExited() and !subprocess.flags.has_stdin_destructor_called) { + pipe.onAttachedProcessExit(); + return pipe.toJS(globalThis); + } else { + subprocess.flags.has_stdin_destructor_called = false; + subprocess.weak_file_sink_stdin_ptr = pipe; + if (@intFromPtr(pipe.signal.ptr) == @intFromPtr(subprocess)) { + pipe.signal.clear(); + } + return pipe.toJSWithDestructor( + globalThis, + JSC.WebCore.SinkDestructor.Ptr.init(subprocess), + ); + } + }, }; } pub fn finalize(this: *Writable) void { + const subprocess = @fieldParentPtr(Subprocess, "stdin", this); + if (subprocess.this_jsvalue != .zero) { + if (JSC.Codegen.JSSubprocess.stdinGetCached(subprocess.this_jsvalue)) |existing_value| { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); + } + } + return switch (this.*) { .pipe => |pipe| { - pipe.close(); + pipe.deref(); + + this.* = .{ .ignore = {} }; }, - .pipe_to_readable_stream => |*pipe_to_readable_stream| { - _ = pipe_to_readable_stream.pipe.end(null); + .buffer => { + this.buffer.updateRef(false); + this.buffer.deref(); }, - inline .memfd, .fd => |fd| { + .memfd => |fd| { _ = bun.sys.close(fd); this.* = .{ .ignore = {} }; }, - .buffered_input => { - this.buffered_input.deinit(); - }, .ignore => {}, - .inherit => {}, + .fd, .inherit => {}, }; } - pub fn setCloseCallbackIfPossible(this: *Writable, callback: CloseCallbackHandler) bool { - switch (this.*) { - .pipe => |pipe| { - if (Environment.isWindows) { - if (pipe.isClosed()) { - return false; - } - pipe.closeCallback = callback; - return true; - } - return false; - }, - .pipe_to_readable_stream => |*pipe_to_readable_stream| { - if (Environment.isWindows) { - if (pipe_to_readable_stream.pipe.isClosed()) { - return false; - } - pipe_to_readable_stream.pipe.closeCallback = callback; - return true; - } - return false; - }, - .buffered_input => { - if (Environment.isWindows) { - this.buffered_input.closeCallback = callback; - return true; - } - return false; - }, - else => return false, - } - } - pub fn close(this: *Writable) void { switch (this.*) { - .pipe => {}, - .pipe_to_readable_stream => |*pipe_to_readable_stream| { - _ = pipe_to_readable_stream.pipe.end(null); + .pipe => |pipe| { + _ = pipe.end(null); }, inline .memfd, .fd => |fd| { _ = bun.sys.close(fd); this.* = .{ .ignore = {} }; }, - .buffered_input => { - this.buffered_input.deinit(); + .buffer => { + this.buffer.close(); }, .ignore => {}, .inherit => {}, @@ -1819,17 +1295,97 @@ pub const Subprocess = struct { } }; - fn closeIOCallback(this: *Subprocess) void { - log("closeIOCallback", .{}); - this.closed_streams += 1; - if (this.closed_streams == @TypeOf(this.closed).len) { - this.exit_promise.deinit(); - this.on_exit_callback.deinit(); - this.stdio_pipes.deinit(bun.default_allocator); + pub fn onProcessExit(this: *Subprocess, _: *Process, status: bun.spawn.Status, rusage: *const Rusage) void { + log("onProcessExit()", .{}); + const this_jsvalue = this.this_jsvalue; + const globalThis = this.globalThis; + this_jsvalue.ensureStillAlive(); + this.pid_rusage = rusage.*; + const is_sync = this.flags.is_sync; + + var stdin: ?*JSC.WebCore.FileSink = this.weak_file_sink_stdin_ptr; + var existing_stdin_value = JSC.JSValue.zero; + if (this_jsvalue != .zero) { + if (JSC.Codegen.JSSubprocess.stdinGetCached(this_jsvalue)) |existing_value| { + if (existing_stdin_value.isCell()) { + if (stdin == null) { + stdin = @as(?*JSC.WebCore.FileSink, @alignCast(@ptrCast(JSC.WebCore.FileSink.JSSink.fromJS(globalThis, existing_value)))); + } + + existing_stdin_value = existing_value; + } + } + } + + if (this.stdin == .buffer) { + this.stdin.buffer.close(); + } + + if (existing_stdin_value != .zero) { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_stdin_value, 0); + } + + if (stdin) |pipe| { + this.weak_file_sink_stdin_ptr = null; + this.flags.has_stdin_destructor_called = true; + pipe.onAttachedProcessExit(); + } + + var did_update_has_pending_activity = false; + defer if (!did_update_has_pending_activity) this.updateHasPendingActivity(); - if (this.deinit_onclose) { - log("destroy", .{}); - bun.default_allocator.destroy(this); + const loop = globalThis.bunVM().eventLoop(); + + if (!is_sync) { + if (this.exit_promise.trySwap()) |promise| { + loop.enter(); + defer loop.exit(); + + if (!did_update_has_pending_activity) { + this.updateHasPendingActivity(); + did_update_has_pending_activity = true; + } + + switch (status) { + .exited => |exited| promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(exited.code)), + .err => |err| promise.asAnyPromise().?.reject(globalThis, err.toJSC(globalThis)), + .signaled => promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(128 +% @intFromEnum(status.signaled))), + else => { + // crash in debug mode + if (comptime Environment.allow_assert) + unreachable; + }, + } + } + + if (this.on_exit_callback.trySwap()) |callback| { + const waitpid_value: JSValue = + if (status == .err) + status.err.toJSC(globalThis) + else + JSC.JSValue.jsUndefined(); + + const this_value = if (this_jsvalue.isEmptyOrUndefinedOrNull()) JSC.JSValue.jsUndefined() else this_jsvalue; + this_value.ensureStillAlive(); + + const args = [_]JSValue{ + this_value, + this.getExitCode(globalThis), + this.getSignalCode(globalThis), + waitpid_value, + }; + + if (!did_update_has_pending_activity) { + this.updateHasPendingActivity(); + did_update_has_pending_activity = true; + } + + loop.runCallback( + callback, + globalThis, + this_value, + &args, + ); } } } @@ -1845,19 +1401,16 @@ pub const Subprocess = struct { // 2. We need to free the memory // 3. We need to halt any pending reads (1) - const closeCallback = CloseCallbackHandler.init(this, @ptrCast(&Subprocess.closeIOCallback)); - const isAsync = @field(this, @tagName(io)).setCloseCallbackIfPossible(closeCallback); - if (!this.hasCalledGetter(io)) { @field(this, @tagName(io)).finalize(); } else { @field(this, @tagName(io)).close(); } + } - if (!isAsync) { - // close is sync - closeCallback.run(); - } + fn onPipeClose(this: *uv.Pipe) callconv(.C) void { + // safely free the pipes + bun.default_allocator.destroy(this); } // This must only be run once per Subprocess @@ -1868,50 +1421,73 @@ pub const Subprocess = struct { this.closeIO(.stdin); this.closeIO(.stdout); this.closeIO(.stderr); + + close_stdio_pipes: { + if (!this.observable_getters.contains(.stdio)) { + break :close_stdio_pipes; + } + + for (this.stdio_pipes.items) |item| { + if (Environment.isWindows) { + if (item == .buffer) { + item.buffer.close(onPipeClose); + } + } else { + _ = bun.sys.close(item); + } + } + this.stdio_pipes.clearAndFree(bun.default_allocator); + } + + this.exit_promise.deinit(); + this.on_exit_callback.deinit(); } pub fn finalize(this: *Subprocess) callconv(.C) void { log("finalize", .{}); - std.debug.assert(!this.hasPendingActivity()); - if (this.closed_streams == @TypeOf(this.closed).len) { - log("destroy", .{}); - bun.default_allocator.destroy(this); - } else { - this.deinit_onclose = true; - this.finalizeStreams(); - } + // Ensure any code which references the "this" value doesn't attempt to + // access it after it's been freed We cannot call any methods which + // access GC'd values during the finalizer + this.this_jsvalue = .zero; + + std.debug.assert(!this.hasPendingActivity() or JSC.VirtualMachine.get().isShuttingDown()); + this.finalizeStreams(); + + this.process.detach(); + this.process.deref(); + bun.default_allocator.destroy(this); } pub fn getExited( this: *Subprocess, globalThis: *JSGlobalObject, ) callconv(.C) JSValue { - if (this.hasExited()) { - const waitpid_error = this.waitpid_err; - if (this.exit_code) |code| { - return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(code)); - } else if (waitpid_error) |err| { + switch (this.process.status) { + .exited => |exit| { + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(exit.code)); + }, + .signaled => |signal| { + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(signal.toExitCode() orelse 254)); + }, + .err => |err| { return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); - } else if (this.signal_code != null) { - return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(128 +% @intFromEnum(this.signal_code.?))); - } else { - @panic("Subprocess.getExited() has exited but has no exit code or signal code. This is a bug."); - } - } + }, + else => { + if (!this.exit_promise.has()) { + this.exit_promise.set(globalThis, JSC.JSPromise.create(globalThis).asValue(globalThis)); + } - if (!this.exit_promise.has()) { - this.exit_promise.set(globalThis, JSC.JSPromise.create(globalThis).asValue(globalThis)); + return this.exit_promise.get().?; + }, } - - return this.exit_promise.get().?; } pub fn getExitCode( this: *Subprocess, _: *JSGlobalObject, ) callconv(.C) JSValue { - if (this.exit_code) |code| { - return JSC.JSValue.jsNumber(code); + if (this.process.status == .exited) { + return JSC.JSValue.jsNumber(this.process.status.exited.code); } return JSC.JSValue.jsNull(); } @@ -1920,7 +1496,7 @@ pub const Subprocess = struct { this: *Subprocess, global: *JSGlobalObject, ) callconv(.C) JSValue { - if (this.signal_code) |signal| { + if (this.process.signalCode()) |signal| { if (signal.name()) |name| return JSC.ZigString.init(name).toValueGC(global) else @@ -1959,33 +1535,27 @@ pub const Subprocess = struct { var stdio = [3]Stdio{ .{ .ignore = {} }, - .{ .pipe = null }, + .{ .pipe = {} }, .{ .inherit = {} }, }; if (comptime is_sync) { - stdio[1] = .{ .pipe = null }; - stdio[2] = .{ .pipe = null }; + stdio[1] = .{ .pipe = {} }; + stdio[2] = .{ .pipe = {} }; } var lazy = false; var on_exit_callback = JSValue.zero; var PATH = jsc_vm.bundler.env.get("PATH") orelse ""; - var argv: std.ArrayListUnmanaged(?[*:0]const u8) = undefined; + var argv = std.ArrayList(?[*:0]const u8).init(allocator); var cmd_value = JSValue.zero; var detached = false; var args = args_; var ipc_mode = IPCMode.none; var ipc_callback: JSValue = .zero; - var stdio_pipes: std.ArrayListUnmanaged(Stdio.PipeExtra) = .{}; - var pipes_to_close: std.ArrayListUnmanaged(bun.FileDescriptor) = .{}; - defer { - for (pipes_to_close.items) |pipe_fd| { - _ = bun.sys.close(pipe_fd); - } - pipes_to_close.clearAndFree(bun.default_allocator); - } + var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); + var argv0: ?[*:0]const u8 = null; - var windows_hide: if (Environment.isWindows) u1 else u0 = 0; + var windows_hide: bool = false; { if (args.isEmptyOrUndefinedOrNull()) { @@ -2000,13 +1570,25 @@ pub const Subprocess = struct { } else if (!args.isObject()) { globalThis.throwInvalidArguments("cmd must be an array", .{}); return .zero; - } else if (args.get(globalThis, "cmd")) |cmd_value_| { + } else if (args.getTruthy(globalThis, "cmd")) |cmd_value_| { cmd_value = cmd_value_; } else { globalThis.throwInvalidArguments("cmd must be an array", .{}); return .zero; } + if (args.isObject()) { + if (args.getTruthy(globalThis, "argv0")) |argv0_| { + const argv0_str = argv0_.getZigString(globalThis); + if (argv0_str.len > 0) { + argv0 = argv0_str.toOwnedSliceZ(allocator) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; + } + } + } + { var cmds_array = cmd_value.arrayIterator(globalThis); argv = @TypeOf(argv).initCapacity(allocator, cmds_array.len) catch { @@ -2028,12 +1610,29 @@ pub const Subprocess = struct { var first_cmd = cmds_array.next().?; var arg0 = first_cmd.toSlice(globalThis, allocator); defer arg0.deinit(); - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const resolved = Which.which(&path_buf, PATH, cwd, arg0.slice()) orelse { - globalThis.throwInvalidArguments("Executable not found in $PATH: \"{s}\"", .{arg0.slice()}); - return .zero; - }; - argv.appendAssumeCapacity(allocator.dupeZ(u8, bun.span(resolved)) catch { + + if (argv0 == null) { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const resolved = Which.which(&path_buf, PATH, cwd, arg0.slice()) orelse { + globalThis.throwInvalidArguments("Executable not found in $PATH: \"{s}\"", .{arg0.slice()}); + return .zero; + }; + argv0 = allocator.dupeZ(u8, resolved) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; + } else { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const resolved = Which.which(&path_buf, PATH, cwd, bun.sliceTo(argv0.?, 0)) orelse { + globalThis.throwInvalidArguments("Executable not found in $PATH: \"{s}\"", .{arg0.slice()}); + return .zero; + }; + argv0 = allocator.dupeZ(u8, resolved) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; + } + argv.appendAssumeCapacity(allocator.dupeZ(u8, arg0.slice()) catch { globalThis.throwOutOfMemory(); return .zero; }); @@ -2060,13 +1659,17 @@ pub const Subprocess = struct { } if (args != .zero and args.isObject()) { - if (args.get(globalThis, "cwd")) |cwd_| { - // ignore definitely invalid cwd - if (!cwd_.isEmptyOrUndefinedOrNull()) { - const cwd_str = cwd_.getZigString(globalThis); - if (cwd_str.len > 0) { - // TODO: leak? - cwd = cwd_str.toOwnedSliceZ(allocator) catch { + + // This must run before the stdio parsing happens + if (args.getTruthy(globalThis, "ipc")) |val| { + if (val.isCell() and val.isCallable(globalThis.vm())) { + // In the future, we should add a way to use a different IPC serialization format, specifically `json`. + // but the only use case this has is doing interop with node.js IPC and other programs. + ipc_mode = .bun; + ipc_callback = val.withAsyncContextIfNeeded(globalThis); + + if (Environment.isPosix) { + extra_fds.append(.{ .buffer = {} }) catch { globalThis.throwOutOfMemory(); return .zero; }; @@ -2074,59 +1677,65 @@ pub const Subprocess = struct { } } - if (args.get(globalThis, "onExit")) |onExit_| { - if (!onExit_.isEmptyOrUndefinedOrNull()) { - if (!onExit_.isCell() or !onExit_.isCallable(globalThis.vm())) { - globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); + if (args.getTruthy(globalThis, "cwd")) |cwd_| { + const cwd_str = cwd_.getZigString(globalThis); + if (cwd_str.len > 0) { + cwd = cwd_str.toOwnedSliceZ(allocator) catch { + globalThis.throwOutOfMemory(); return .zero; - } - - on_exit_callback = if (comptime is_sync) - onExit_ - else - onExit_.withAsyncContextIfNeeded(globalThis); + }; } } - if (args.get(globalThis, "env")) |object| { - if (!object.isEmptyOrUndefinedOrNull()) { - if (!object.isObject()) { - globalThis.throwInvalidArguments("env must be an object", .{}); - return .zero; - } + if (args.getTruthy(globalThis, "onExit")) |onExit_| { + if (!onExit_.isCell() or !onExit_.isCallable(globalThis.vm())) { + globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); + return .zero; + } - override_env = true; - var object_iter = JSC.JSPropertyIterator(.{ - .skip_empty_name = false, - .include_value = true, - }).init(globalThis, object.asObjectRef()); - defer object_iter.deinit(); - env_array.ensureTotalCapacityPrecise(allocator, object_iter.len) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; + on_exit_callback = if (comptime is_sync) + onExit_ + else + onExit_.withAsyncContextIfNeeded(globalThis); + } + + if (args.getTruthy(globalThis, "env")) |object| { + if (!object.isObject()) { + globalThis.throwInvalidArguments("env must be an object", .{}); + return .zero; + } - // If the env object does not include a $PATH, it must disable path lookup for argv[0] - PATH = ""; + override_env = true; + var object_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(globalThis, object.asObjectRef()); + defer object_iter.deinit(); + env_array.ensureTotalCapacityPrecise(allocator, object_iter.len) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; - while (object_iter.next()) |key| { - var value = object_iter.value; - if (value == .undefined) continue; + // If the env object does not include a $PATH, it must disable path lookup for argv[0] + PATH = ""; - var line = std.fmt.allocPrintZ(allocator, "{}={}", .{ key, value.getZigString(globalThis) }) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; + while (object_iter.next()) |key| { + var value = object_iter.value; + if (value == .undefined) continue; - if (key.eqlComptime("PATH")) { - PATH = bun.asByteSlice(line["PATH=".len..]); - } + var line = std.fmt.allocPrintZ(allocator, "{}={}", .{ key, value.getZigString(globalThis) }) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; - env_array.append(allocator, line) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; + if (key.eqlComptime("PATH")) { + PATH = bun.asByteSlice(line["PATH=".len..]); } + + env_array.append(allocator, line) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; } } @@ -2136,7 +1745,7 @@ pub const Subprocess = struct { var stdio_iter = stdio_val.arrayIterator(globalThis); var i: u32 = 0; while (stdio_iter.next()) |value| : (i += 1) { - if (!extractStdio(globalThis, i, value, &stdio[i])) + if (!stdio[i].extract(globalThis, i, value)) return JSC.JSValue.jsUndefined(); if (i == 2) break; @@ -2145,20 +1754,20 @@ pub const Subprocess = struct { while (stdio_iter.next()) |value| : (i += 1) { var new_item: Stdio = undefined; - if (!extractStdio(globalThis, i, value, &new_item)) + if (!new_item.extract(globalThis, i, value)) { return JSC.JSValue.jsUndefined(); - switch (new_item) { - .pipe => { - stdio_pipes.append(bun.default_allocator, .{ - .fd = 0, - .fileno = @intCast(i), - }) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; - }, - else => {}, } + + const opt = switch (new_item.asSpawnOption(i)) { + .result => |opt| opt, + .err => |e| { + return e.throwJS(globalThis); + }, + }; + extra_fds.append(opt) catch { + globalThis.throwOutOfMemory(); + return .zero; + }; } } else { globalThis.throwInvalidArguments("stdio must be an array", .{}); @@ -2167,17 +1776,17 @@ pub const Subprocess = struct { } } else { if (args.get(globalThis, "stdin")) |value| { - if (!extractStdio(globalThis, 0, value, &stdio[0])) + if (!stdio[0].extract(globalThis, 0, value)) return .zero; } if (args.get(globalThis, "stderr")) |value| { - if (!extractStdio(globalThis, 2, value, &stdio[2])) + if (!stdio[2].extract(globalThis, 2, value)) return .zero; } if (args.get(globalThis, "stdout")) |value| { - if (!extractStdio(globalThis, 1, value, &stdio[1])) + if (!stdio[1].extract(globalThis, 1, value)) return .zero; } } @@ -2196,209 +1805,19 @@ pub const Subprocess = struct { } } - if (args.getTruthy(globalThis, "ipc")) |val| { - if (Environment.isWindows) { - globalThis.throwTODO("TODO: IPC is not yet supported on Windows"); - return .zero; - } - - if (val.isCell() and val.isCallable(globalThis.vm())) { - // In the future, we should add a way to use a different IPC serialization format, specifically `json`. - // but the only use case this has is doing interop with node.js IPC and other programs. - ipc_mode = .bun; - ipc_callback = val.withAsyncContextIfNeeded(globalThis); - } - } - if (Environment.isWindows) { if (args.get(globalThis, "windowsHide")) |val| { if (val.isBoolean()) { - windows_hide = @intFromBool(val.asBoolean()); + windows_hide = val.asBoolean(); } } } } } - // WINDOWS: - if (Environment.isWindows) { - argv.append(allocator, null) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; - - if (!override_env and env_array.items.len == 0) { - env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.handleError(err, "in posix_spawn"); - env_array.capacity = env_array.items.len; - } - - env_array.append(allocator, null) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; - const env: [*:null]?[*:0]const u8 = @ptrCast(env_array.items.ptr); - - const alloc = globalThis.allocator(); - var subprocess = alloc.create(Subprocess) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; - - var uv_stdio = [3]uv.uv_stdio_container_s{ - stdio[0].setUpChildIoUvSpawn(0, &subprocess.pipes[0], true, bun.invalid_fd) catch |err| { - alloc.destroy(subprocess); - return globalThis.handleError(err, "in setting up uv_process stdin"); - }, - stdio[1].setUpChildIoUvSpawn(1, &subprocess.pipes[1], false, bun.invalid_fd) catch |err| { - alloc.destroy(subprocess); - return globalThis.handleError(err, "in setting up uv_process stdout"); - }, - stdio[2].setUpChildIoUvSpawn(2, &subprocess.pipes[2], false, bun.invalid_fd) catch |err| { - alloc.destroy(subprocess); - return globalThis.handleError(err, "in setting up uv_process stderr"); - }, - }; - - var cwd_resolver = bun.path.PosixToWinNormalizer{}; - - const options = uv.uv_process_options_t{ - .exit_cb = uvExitCallback, - .args = @ptrCast(argv.items[0 .. argv.items.len - 1 :null]), - .cwd = cwd_resolver.resolveCWDZ(cwd) catch |err| { - alloc.destroy(subprocess); - return globalThis.handleError(err, "in uv_spawn"); - }, - .env = env, - .file = argv.items[0].?, - .gid = 0, - .uid = 0, - .stdio = &uv_stdio, - .stdio_count = uv_stdio.len, - .flags = if (windows_hide == 1) uv.UV_PROCESS_WINDOWS_HIDE else 0, - }; - - const loop = jsc_vm.uvLoop(); - if (uv.uv_spawn(loop, &subprocess.pid, &options).errEnum()) |errno| { - alloc.destroy(subprocess); - globalThis.throwValue(bun.sys.Error.fromCode(errno, .uv_spawn).toJSC(globalThis)); - return .zero; - } - - // When run synchronously, subprocess isn't garbage collected - subprocess.* = Subprocess{ - .pipes = subprocess.pipes, - .globalThis = globalThis, - .pid = subprocess.pid, - .pidfd = 0, - .stdin = Writable.initWithPipe(stdio[0], &subprocess.pipes[0], globalThis) catch { - globalThis.throwOutOfMemory(); - return .zero; - }, - // stdout and stderr only uses allocator and default_max_buffer_size if they are pipes and not a array buffer - .stdout = Readable.initWithPipe(stdio[1], &subprocess.pipes[1], jsc_vm.allocator, default_max_buffer_size), - .stderr = Readable.initWithPipe(stdio[2], &subprocess.pipes[2], jsc_vm.allocator, default_max_buffer_size), - .on_exit_callback = if (on_exit_callback != .zero) JSC.Strong.create(on_exit_callback, globalThis) else .{}, - - .ipc_mode = ipc_mode, - .ipc = undefined, - .ipc_callback = undefined, - - .flags = .{ - .is_sync = is_sync, - }, - }; - subprocess.pid.data = subprocess; - std.debug.assert(ipc_mode == .none); //TODO: - - const out = if (comptime !is_sync) subprocess.toJS(globalThis) else .zero; - subprocess.this_jsvalue = out; - - if (subprocess.stdin == .buffered_input) { - subprocess.stdin.buffered_input.remain = switch (subprocess.stdin.buffered_input.source) { - .blob => subprocess.stdin.buffered_input.source.blob.slice(), - .array_buffer => |array_buffer| array_buffer.slice(), - }; - subprocess.stdin.buffered_input.writeIfPossible(is_sync); - } - - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - if (is_sync or !lazy) { - subprocess.stdout.pipe.buffer.readAll(); - } - } - - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - if (is_sync or !lazy) { - subprocess.stderr.pipe.buffer.readAll(); - } - } - - if (comptime !is_sync) { - return out; - } - - // sync - - while (!subprocess.hasExited()) { - loop.tickWithTimeout(0); - - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - subprocess.stderr.pipe.buffer.readAll(); - } - - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - subprocess.stdout.pipe.buffer.readAll(); - } - } - - const exitCode = subprocess.exit_code orelse 1; - const stdout = subprocess.stdout.toBufferedValue(globalThis); - const stderr = subprocess.stderr.toBufferedValue(globalThis); - const resource_usage = subprocess.createResourceUsageObject(globalThis); - subprocess.finalizeStreams(); - - const sync_value = JSC.JSValue.createEmptyObject(globalThis, 5); - sync_value.put(globalThis, JSC.ZigString.static("exitCode"), JSValue.jsNumber(@as(i32, @intCast(exitCode)))); - sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); - sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); - sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode == 0)); - sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); - return sync_value; - } - // POSIX: - - var attr = PosixSpawn.Attr.init() catch { - globalThis.throwOutOfMemory(); - return .zero; - }; - - var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK; - - if (comptime Environment.isMac) { - flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; - } - - if (detached) { - flags |= bun.C.POSIX_SPAWN_SETSID; - } - - defer attr.deinit(); - var actions = PosixSpawn.Actions.init() catch |err| return globalThis.handleError(err, "in posix_spawn"); - if (comptime Environment.isMac) { - attr.set(@intCast(flags)) catch |err| return globalThis.handleError(err, "in posix_spawn"); - } else if (comptime Environment.isLinux) { - attr.set(@intCast(flags)) catch |err| return globalThis.handleError(err, "in posix_spawn"); - } - - attr.resetSignals() catch { - globalThis.throw("Failed to reset signals in posix_spawn", .{}); - return .zero; - }; - - defer actions.deinit(); - if (!override_env and env_array.items.len == 0) { - env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.handleError(err, "in posix_spawn"); + env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch |err| + return globalThis.handleError(err, "in Bun.spawn"); env_array.capacity = env_array.items.len; } @@ -2420,238 +1839,182 @@ pub const Subprocess = struct { } } - // TODO: move pipe2 to bun.sys so it can return [2]bun.FileDesriptor - const stdin_pipe = if (stdio[0].isPiped()) bun.sys.pipe().unwrap() catch |err| { - globalThis.throw("failed to create stdin pipe: {s}", .{@errorName(err)}); - return .zero; - } else undefined; + var windows_ipc_env_buf: if (Environment.isWindows) ["BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_00000000-0000-0000-0000-000000000000".len]u8 else void = undefined; + if (ipc_mode != .none) { + if (comptime is_sync) { + globalThis.throwInvalidArguments("IPC is not supported in Bun.spawnSync", .{}); + return .zero; + } - const stdout_pipe = if (stdio[1].isPiped()) bun.sys.pipe().unwrap() catch |err| { - globalThis.throw("failed to create stdout pipe: {s}", .{@errorName(err)}); - return .zero; - } else undefined; + // IPC is currently implemented in a very limited way. + // + // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special + // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). + // + // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional + // + // And then fd 3 is assigned specifically and only for IPC. This is quite lame, because Node.js allows + // the ipc fd to be any number and it just works. But most people only care about the default `.fork()` + // behavior, where this workaround suffices. + // + // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: + env_array.ensureUnusedCapacity(allocator, 2) catch |err| return globalThis.handleError(err, "in Bun.spawn"); + if (Environment.isPosix) { + env_array.appendAssumeCapacity("BUN_INTERNAL_IPC_FD=3"); + } else { + const uuid = globalThis.bunVM().rareData().nextUUID(); + const pipe_env = std.fmt.bufPrintZ(&windows_ipc_env_buf, "BUN_INTERNAL_IPC_FD=\\\\.\\pipe\\BUN_IPC_{s}", .{uuid}) catch |err| switch (err) { + error.NoSpaceLeft => unreachable, // upper bound for this string is known + }; + env_array.appendAssumeCapacity(pipe_env); + } + } - const stderr_pipe = if (stdio[2].isPiped()) bun.sys.pipe().unwrap() catch |err| { - globalThis.throw("failed to create stderr pipe: {s}", .{@errorName(err)}); + env_array.append(allocator, null) catch { + globalThis.throwOutOfMemory(); return .zero; - } else undefined; - - stdio[0].setUpChildIoPosixSpawn( - &actions, - stdin_pipe, - bun.STDIN_FD, - ) catch |err| return globalThis.handleError(err, "in configuring child stdin"); - - stdio[1].setUpChildIoPosixSpawn( - &actions, - stdout_pipe, - bun.STDOUT_FD, - ) catch |err| return globalThis.handleError(err, "in configuring child stdout"); - - stdio[2].setUpChildIoPosixSpawn( - &actions, - stderr_pipe, - bun.STDERR_FD, - ) catch |err| return globalThis.handleError(err, "in configuring child stderr"); - - for (stdio_pipes.items) |*item| { - const maybe = blk: { - // TODO: move this to bun.sys so it can return [2]bun.FileDesriptor - var fds: [2]c_int = undefined; - const socket_type = os.SOCK.STREAM; - const rc = std.os.system.socketpair(os.AF.UNIX, socket_type, 0, &fds); - switch (std.os.system.getErrno(rc)) { - .SUCCESS => {}, - .AFNOSUPPORT => break :blk error.AddressFamilyNotSupported, - .FAULT => break :blk error.Fault, - .MFILE => break :blk error.ProcessFdQuotaExceeded, - .NFILE => break :blk error.SystemFdQuotaExceeded, - .OPNOTSUPP => break :blk error.OperationNotSupported, - .PROTONOSUPPORT => break :blk error.ProtocolNotSupported, - else => |err| break :blk std.os.unexpectedErrno(err), - } - pipes_to_close.append(bun.default_allocator, bun.toFD(fds[1])) catch |err| break :blk err; - actions.dup2(bun.toFD(fds[1]), bun.toFD(item.fileno)) catch |err| break :blk err; - actions.close(bun.toFD(fds[1])) catch |err| break :blk err; - item.fd = fds[0]; - // enable non-block - const before = std.c.fcntl(fds[0], os.F.GETFL); - _ = std.c.fcntl(fds[0], os.F.SETFL, before | os.O.NONBLOCK); - // enable SOCK_CLOXEC - _ = std.c.fcntl(fds[0], os.FD_CLOEXEC); - }; - _ = maybe catch |err| return globalThis.handleError(err, "in configuring child stderr"); - } - - actions.chdir(cwd) catch |err| return globalThis.handleError(err, "in chdir()"); - - argv.append(allocator, null) catch { + }; + argv.append(null) catch { globalThis.throwOutOfMemory(); return .zero; }; - // IPC is currently implemented in a very limited way. - // - // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special - // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). - // - // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional - // - // And then fd 3 is assigned specifically and only for IPC. This is quite lame, because Node.js allows - // the ipc fd to be any number and it just works. But most people only care about the default `.fork()` - // behavior, where this workaround suffices. - // - // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: - var socket: IPC.Socket = undefined; - if (ipc_mode != .none) { - if (comptime is_sync) { - globalThis.throwInvalidArguments("IPC is not supported in Bun.spawnSync", .{}); - return .zero; + if (comptime is_sync) { + for (&stdio, 0..) |*io, i| { + io.toSync(@truncate(i)); } + } - env_array.ensureUnusedCapacity(allocator, 2) catch |err| return globalThis.handleError(err, "in posix_spawn"); - env_array.appendAssumeCapacity("BUN_INTERNAL_IPC_FD=3"); + const spawn_options = bun.spawn.SpawnOptions{ + .cwd = cwd, + .detached = detached, + .stdin = switch (stdio[0].asSpawnOption(0)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stdout = switch (stdio[1].asSpawnOption(1)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stderr = switch (stdio[2].asSpawnOption(2)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .extra_fds = extra_fds.items, + .argv0 = argv0, - var fds: [2]uws.LIBUS_SOCKET_DESCRIPTOR = undefined; - socket = uws.newSocketFromPair( - jsc_vm.rareData().spawnIPCContext(jsc_vm), - @sizeOf(*Subprocess), - &fds, - ) orelse { - globalThis.throw("failed to create socket pair: E{s}", .{ - @tagName(bun.sys.getErrno(-1)), - }); - return .zero; - }; - socket.setTimeout(0); - pipes_to_close.append(bun.default_allocator, bun.toFD(fds[1])) catch |err| return globalThis.handleError(err, "in posix_spawn"); - actions.dup2(bun.toFD(fds[1]), bun.toFD(3)) catch |err| return globalThis.handleError(err, "in posix_spawn"); - actions.close(bun.toFD(fds[1])) catch |err| return globalThis.handleError(err, "in posix_spawn"); - // enable non-block - const before = std.c.fcntl(fds[0], os.F.GETFL); - _ = std.c.fcntl(fds[0], os.F.SETFL, before | os.O.NONBLOCK); - // enable SOCK_CLOXEC - _ = std.c.fcntl(fds[0], os.FD_CLOEXEC); - } + .windows = if (Environment.isWindows) .{ + .hide_window = windows_hide, + .loop = JSC.EventLoopHandle.init(jsc_vm), + } else {}, + }; - env_array.append(allocator, null) catch { + const process_allocator = globalThis.allocator(); + var subprocess = process_allocator.create(Subprocess) catch { globalThis.throwOutOfMemory(); return .zero; }; - const env: [*:null]?[*:0]const u8 = @ptrCast(env_array.items.ptr); - - const pid = brk: { - defer { - if (stdio[0].isPiped()) { - _ = bun.sys.close(bun.toFD(stdin_pipe[0])); - } - if (stdio[1].isPiped()) { - _ = bun.sys.close(bun.toFD(stdout_pipe[1])); - } - if (stdio[2].isPiped()) { - _ = bun.sys.close(bun.toFD(stderr_pipe[1])); - } - // we always close these, but we want to close these earlier - for (pipes_to_close.items) |pipe_fd| { - _ = bun.sys.close(pipe_fd); - } - pipes_to_close.clearAndFree(bun.default_allocator); - } + var spawned = switch (bun.spawn.spawnProcess( + &spawn_options, + @ptrCast(argv.items.ptr), + @ptrCast(env_array.items.ptr), + ) catch |err| { + process_allocator.destroy(subprocess); + spawn_options.deinit(); + globalThis.throwError(err, ": failed to spawn process"); - break :brk switch (PosixSpawn.spawnZ(argv.items[0].?, actions, attr, @as([*:null]?[*:0]const u8, @ptrCast(argv.items[0..].ptr)), env)) { - .err => |err| { - globalThis.throwValue(err.toJSC(globalThis)); - return .zero; - }, - .result => |pid_| pid_, - }; + return .zero; + }) { + .err => |err| { + process_allocator.destroy(subprocess); + spawn_options.deinit(); + globalThis.throwValue(err.toJSC(globalThis)); + return .zero; + }, + .result => |result| result, }; - var rusage_result: Rusage = std.mem.zeroes(Rusage); - var has_rusage = false; - const pidfd: std.os.fd_t = brk: { - if (!Environment.isLinux or WaiterThread.shouldUseWaiterThread()) { - break :brk pid; - } - - var pidfd_flags = pidfdFlagsForLinux(); - - var rc = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - while (true) { - switch (std.os.linux.getErrno(rc)) { - .SUCCESS => break :brk @as(std.os.fd_t, @intCast(rc)), - .INTR => { - rc = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - continue; - }, - else => |err| { - if (err == .INVAL) { - if (pidfd_flags != 0) { - rc = std.os.linux.pidfd_open( - @intCast(pid), - 0, - ); - pidfd_flags = 0; - continue; - } - } - - const error_instance = brk2: { - if (err == .NOSYS) { - WaiterThread.setShouldUseWaiterThread(); - break :brk pid; - } - - break :brk2 bun.sys.Error.fromCode(err, .open).toJSC(globalThis); - }; - globalThis.throwValue(error_instance); - var status: u32 = 0; - // ensure we don't leak the child process on error - _ = std.os.linux.wait4(pid, &status, 0, &rusage_result); - has_rusage = true; + var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; + if (Environment.isPosix) { + if (ipc_mode != .none) { + posix_ipc_info = .{ + // we initialize ext later in the function + .socket = uws.us_socket_from_fd( + jsc_vm.rareData().spawnIPCContext(jsc_vm), + @sizeOf(*Subprocess), + spawned.extra_pipes.items[0].cast(), + ) orelse { + process_allocator.destroy(subprocess); + spawn_options.deinit(); + globalThis.throw("failed to create socket pair", .{}); return .zero; }, - } + }; } - }; + } + + const loop = jsc_vm.eventLoop(); - var subprocess = globalThis.allocator().create(Subprocess) catch { - globalThis.throwOutOfMemory(); - return .zero; - }; // When run synchronously, subprocess isn't garbage collected subprocess.* = Subprocess{ .globalThis = globalThis, - .pid = pid, - .pid_rusage = if (has_rusage) rusage_result else null, - .pidfd = if (WaiterThread.shouldUseWaiterThread()) @truncate(bun.invalid_fd.int()) else @truncate(pidfd), - .stdin = Writable.init(stdio[0], bun.toFD(stdin_pipe[1]), globalThis) catch { + .process = spawned.toProcess(loop, is_sync), + .pid_rusage = null, + .stdin = Writable.init( + stdio[0], + loop, + subprocess, + spawned.stdin, + ) catch { globalThis.throwOutOfMemory(); return .zero; }, - // stdout and stderr only uses allocator and default_max_buffer_size if they are pipes and not a array buffer - .stdout = Readable.init(stdio[1], bun.toFD(stdout_pipe[0]), jsc_vm.allocator, default_max_buffer_size), - .stderr = Readable.init(stdio[2], bun.toFD(stderr_pipe[0]), jsc_vm.allocator, default_max_buffer_size), - .stdio_pipes = stdio_pipes, + .stdout = Readable.init( + stdio[1], + loop, + subprocess, + spawned.stdout, + jsc_vm.allocator, + default_max_buffer_size, + is_sync, + ), + .stderr = Readable.init( + stdio[2], + loop, + subprocess, + spawned.stderr, + jsc_vm.allocator, + default_max_buffer_size, + is_sync, + ), + .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), .on_exit_callback = if (on_exit_callback != .zero) JSC.Strong.create(on_exit_callback, globalThis) else .{}, .ipc_mode = ipc_mode, // will be assigned in the block below - .ipc = .{ .socket = socket }, + .ipc = if (Environment.isWindows) .{} else .{ .socket = posix_ipc_info }, .ipc_callback = if (ipc_callback != .zero) JSC.Strong.create(ipc_callback, globalThis) else undefined, .flags = .{ .is_sync = is_sync, }, }; + subprocess.process.setExitHandler(subprocess); + if (ipc_mode != .none) { - const ptr = socket.ext(*Subprocess); - ptr.?.* = subprocess; + if (Environment.isPosix) { + const ptr = posix_ipc_info.ext(*Subprocess); + ptr.?.* = subprocess; + } else { + if (subprocess.ipc.configureServer( + Subprocess, + subprocess, + windows_ipc_env_buf["BUN_INTERNAL_IPC_FD=".len..], + ).asErr()) |err| { + process_allocator.destroy(subprocess); + globalThis.throwValue(err.toJSC(globalThis)); + return .zero; + } + } subprocess.ipc.writeVersionPacket(); } @@ -2666,31 +2029,14 @@ pub const Subprocess = struct { subprocess.this_jsvalue = out; var send_exit_notification = false; - const watchfd = if (comptime Environment.isLinux) pidfd else pid; if (comptime !is_sync) { - if (!WaiterThread.shouldUseWaiterThread()) { - const poll = Async.FilePoll.init(jsc_vm, bun.toFD(watchfd), .{}, Subprocess, subprocess); - subprocess.poll = .{ .poll_ref = poll }; - switch (subprocess.poll.poll_ref.?.register( - jsc_vm.event_loop_handle.?, - .process, - true, - )) { - .result => { - subprocess.poll.poll_ref.?.enableKeepingProcessAlive(jsc_vm); - }, - .err => |err| { - if (err.getErrno() != .SRCH) { - @panic("This shouldn't happen"); - } - - send_exit_notification = true; - lazy = false; - }, - } - } else { - WaiterThread.append(subprocess); + switch (subprocess.process.watch(jsc_vm)) { + .result => {}, + .err => { + send_exit_notification = true; + lazy = false; + }, } } @@ -2698,27 +2044,26 @@ pub const Subprocess = struct { if (send_exit_notification) { // process has already exited // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.wait(subprocess.flags.is_sync); + subprocess.process.wait(is_sync); } } - if (subprocess.stdin == .buffered_input) { - subprocess.stdin.buffered_input.remain = switch (subprocess.stdin.buffered_input.source) { - .blob => subprocess.stdin.buffered_input.source.blob.slice(), - .array_buffer => |array_buffer| array_buffer.slice(), - }; - subprocess.stdin.buffered_input.writeIfPossible(is_sync); + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.start().assert(); } - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - if (is_sync or !lazy) { - subprocess.stdout.pipe.buffer.readAll(); + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.start(subprocess, loop).assert(); + if ((is_sync or !lazy) and subprocess.stdout == .pipe) { + subprocess.stdout.pipe.readAll(); } } - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - if (is_sync or !lazy) { - subprocess.stderr.pipe.buffer.readAll(); + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.start(subprocess, loop).assert(); + + if ((is_sync or !lazy) and subprocess.stderr == .pipe) { + subprocess.stderr.pipe.readAll(); } } @@ -2728,644 +2073,55 @@ pub const Subprocess = struct { return out; } - if (subprocess.stdin == .buffered_input) { - while (subprocess.stdin.buffered_input.remain.len > 0) { - subprocess.stdin.buffered_input.writeIfPossible(true); + if (comptime is_sync) { + switch (subprocess.process.watch(jsc_vm)) { + .result => {}, + .err => { + subprocess.process.wait(true); + }, } } - subprocess.closeIO(.stdin); - - if (!WaiterThread.shouldUseWaiterThread()) { - const poll = Async.FilePoll.init(jsc_vm, bun.toFD(watchfd), .{}, Subprocess, subprocess); - subprocess.poll = .{ .poll_ref = poll }; - switch (subprocess.poll.poll_ref.?.register( - jsc_vm.event_loop_handle.?, - .process, - true, - )) { - .result => { - subprocess.poll.poll_ref.?.enableKeepingProcessAlive(jsc_vm); - }, - .err => |err| { - if (err.getErrno() != .SRCH) { - @panic("This shouldn't happen"); - } - - // process has already exited - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.onExitNotification(); - }, + while (subprocess.hasPendingActivityNonThreadsafe()) { + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.watch(); } - } else { - WaiterThread.append(subprocess); - } - while (!subprocess.hasExited()) { - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - subprocess.stderr.pipe.buffer.readAll(); + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.watch(); } - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - subprocess.stdout.pipe.buffer.readAll(); + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.watch(); } jsc_vm.tick(); jsc_vm.eventLoop().autoTick(); } - const exitCode = subprocess.exit_code orelse 1; + subprocess.updateHasPendingActivity(); + + const signalCode = subprocess.getSignalCode(globalThis); + const exitCode = subprocess.getExitCode(globalThis); const stdout = subprocess.stdout.toBufferedValue(globalThis); const stderr = subprocess.stderr.toBufferedValue(globalThis); const resource_usage = subprocess.createResourceUsageObject(globalThis); - subprocess.finalizeStreams(); + subprocess.finalize(); - const sync_value = JSC.JSValue.createEmptyObject(globalThis, 5); - sync_value.put(globalThis, JSC.ZigString.static("exitCode"), JSValue.jsNumber(@as(i32, @intCast(exitCode)))); + const sync_value = JSC.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull()))); + sync_value.put(globalThis, JSC.ZigString.static("exitCode"), exitCode); + if (!signalCode.isEmptyOrUndefinedOrNull()) { + sync_value.put(globalThis, JSC.ZigString.static("signalCode"), signalCode); + } sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); - sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode == 0)); + sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0)); sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); return sync_value; } - pub fn onExitNotificationTask(this: *Subprocess) void { - var vm = this.globalThis.bunVM(); - const is_sync = this.flags.is_sync; - - if (!is_sync) vm.eventLoop().enter(); - defer if (!is_sync) vm.eventLoop().exit(); - - this.wait(false); - } - - pub fn onExitNotification( - this: *Subprocess, - ) void { - std.debug.assert(this.flags.is_sync); - - this.wait(this.flags.is_sync); - } - - pub fn wait(this: *Subprocess, sync: bool) void { - if (Environment.isWindows) { - @panic("TODO: Windows"); - } - return this.waitWithJSValue(sync, this.this_jsvalue); - } - - pub fn watch(this: *Subprocess) JSC.Maybe(void) { - if (WaiterThread.shouldUseWaiterThread()) { - WaiterThread.append(this); - return JSC.Maybe(void){ .result = {} }; - } - - if (this.poll.poll_ref) |poll| { - const registration = poll.register( - this.globalThis.bunVM().event_loop_handle.?, - .process, - true, - ); - - return registration; - } else { - @panic("Internal Bun error: poll_ref in Subprocess is null unexpectedly. Please file a bug report."); - } - } - - pub fn waitWithJSValue( - this: *Subprocess, - sync: bool, - this_jsvalue: JSC.JSValue, - ) void { - var rusage_result: Rusage = std.mem.zeroes(Rusage); - this.onWaitPid(sync, this_jsvalue, PosixSpawn.wait4(this.pid, if (sync) 0 else std.os.W.NOHANG, &rusage_result), rusage_result); - } - - pub fn onWaitPid(this: *Subprocess, sync: bool, this_jsvalue: JSC.JSValue, waitpid_result_: JSC.Maybe(PosixSpawn.WaitPidResult), pid_rusage: Rusage) void { - if (Environment.isWindows) { - @panic("TODO: Windows"); - } - defer if (sync) this.updateHasPendingActivity(); - - const pid = this.pid; - - var waitpid_result = waitpid_result_; - var rusage_result = pid_rusage; - - while (true) { - switch (waitpid_result) { - .err => |err| { - this.waitpid_err = err; - }, - .result => |result| { - if (result.pid == pid) { - this.pid_rusage = rusage_result; - if (std.os.W.IFEXITED(result.status)) { - this.exit_code = @as(u8, @truncate(std.os.W.EXITSTATUS(result.status))); - } - - // True if the process terminated due to receipt of a signal. - if (std.os.W.IFSIGNALED(result.status)) { - this.signal_code = @as(SignalCode, @enumFromInt(@as(u8, @truncate(std.os.W.TERMSIG(result.status))))); - } else if ( - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/waitpid.2.html - // True if the process has not terminated, but has stopped and can - // be restarted. This macro can be true only if the wait call spec-ified specified - // ified the WUNTRACED option or if the child process is being - // traced (see ptrace(2)). - std.os.W.IFSTOPPED(result.status)) { - this.signal_code = @as(SignalCode, @enumFromInt(@as(u8, @truncate(std.os.W.STOPSIG(result.status))))); - } - } - - if (!this.hasExited()) { - switch (this.watch()) { - .result => {}, - .err => |err| { - if (comptime Environment.isMac) { - if (err.getErrno() == .SRCH) { - waitpid_result = PosixSpawn.wait4(pid, if (sync) 0 else std.os.W.NOHANG, &rusage_result); - continue; - } - } - }, - } - } - }, - } - break; - } - - if (!sync and this.hasExited()) { - const vm = this.globalThis.bunVM(); - - // prevent duplicate notifications - switch (this.poll) { - .poll_ref => |poll_| { - if (poll_) |poll| { - this.poll.poll_ref = null; - poll.deinitWithVM(vm); - } - }, - .wait_thread => { - this.poll.wait_thread.poll_ref.deactivate(vm.event_loop_handle.?); - }, - } - - this.onExit(this.globalThis, this_jsvalue); - } - } - - fn uvExitCallback(process: *uv.uv_process_t, exit_status: i64, term_signal: c_int) callconv(.C) void { - const subprocess: *Subprocess = @alignCast(@ptrCast(process.data.?)); - subprocess.globalThis.assertOnJSThread(); - subprocess.exit_code = @as(u8, @truncate(@as(u64, @intCast(exit_status)))); - subprocess.signal_code = if (term_signal > 0 and term_signal < @intFromEnum(SignalCode.SIGSYS)) @enumFromInt(term_signal) else null; - subprocess.pid_rusage = uv_getrusage(process); - subprocess.onExit(subprocess.globalThis, subprocess.this_jsvalue); - } - - fn runOnExit(this: *Subprocess, globalThis: *JSC.JSGlobalObject, this_jsvalue: JSC.JSValue) void { - const waitpid_error = this.waitpid_err; - this.waitpid_err = null; - - if (this.exit_promise.trySwap()) |promise| { - if (this.exit_code) |code| { - promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(code)); - } else if (waitpid_error) |err| { - promise.asAnyPromise().?.reject(globalThis, err.toJSC(globalThis)); - } else if (this.signal_code != null) { - promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(128 +% @intFromEnum(this.signal_code.?))); - } else { - // crash in debug mode - if (comptime Environment.allow_assert) - unreachable; - } - } - - if (this.on_exit_callback.trySwap()) |callback| { - const waitpid_value: JSValue = - if (waitpid_error) |err| - err.toJSC(globalThis) - else - JSC.JSValue.jsUndefined(); - - const this_value = if (this_jsvalue.isEmptyOrUndefinedOrNull()) JSC.JSValue.jsUndefined() else this_jsvalue; - this_value.ensureStillAlive(); - - const args = [_]JSValue{ - this_value, - this.getExitCode(globalThis), - this.getSignalCode(globalThis), - waitpid_value, - }; - - globalThis.bunVM().eventLoop().runCallback( - callback, - globalThis, - this_value, - &args, - ); - } - } - - fn onExit( - this: *Subprocess, - globalThis: *JSC.JSGlobalObject, - this_jsvalue: JSC.JSValue, - ) void { - log("onExit({d}) = {d}, \"{s}\"", .{ - if (Environment.isWindows) this.pid.pid else this.pid, - if (this.exit_code) |e| @as(i32, @intCast(e)) else -1, - if (this.signal_code) |code| @tagName(code) else "", - }); - defer this.updateHasPendingActivity(); - this_jsvalue.ensureStillAlive(); - - if (this.hasExited()) { - { - this.flags.waiting_for_onexit = true; - - const Holder = struct { - process: *Subprocess, - task: JSC.AnyTask, - - pub fn unref(self: *@This()) void { - // this calls disableKeepingProcessAlive on pool_ref and stdin, stdout, stderr - self.process.flags.waiting_for_onexit = false; - self.process.unref(true); - self.process.updateHasPendingActivity(); - bun.default_allocator.destroy(self); - } - }; - - var holder = bun.default_allocator.create(Holder) catch bun.outOfMemory(); - - holder.* = .{ - .process = this, - .task = JSC.AnyTask.New(Holder, Holder.unref).init(holder), - }; - - this.globalThis.bunVM().enqueueTask(JSC.Task.init(&holder.task)); - } - - this.runOnExit(globalThis, this_jsvalue); - } - } - const os = std.os; - fn destroyPipe(pipe: [2]os.fd_t) void { - os.close(pipe[0]); - if (pipe[0] != pipe[1]) os.close(pipe[1]); - } - - const Stdio = union(enum) { - inherit: void, - ignore: void, - fd: bun.FileDescriptor, - path: JSC.Node.PathLike, - blob: JSC.WebCore.AnyBlob, - pipe: ?JSC.WebCore.ReadableStream, - array_buffer: JSC.ArrayBuffer.Strong, - memfd: bun.FileDescriptor, - - const PipeExtra = struct { - fd: i32, - fileno: i32, - }; - - pub fn canUseMemfd(this: *const @This(), is_sync: bool) bool { - if (comptime !Environment.isLinux) { - return false; - } - - return switch (this.*) { - .blob => !this.blob.needsToReadFile(), - .memfd, .array_buffer => true, - .pipe => |pipe| pipe == null and is_sync, - else => false, - }; - } - - pub fn byteSlice(this: *const @This()) []const u8 { - return switch (this.*) { - .blob => this.blob.slice(), - .array_buffer => |array_buffer| array_buffer.slice(), - else => "", - }; - } - - pub fn useMemfd(this: *@This(), index: u32) void { - const label = switch (index) { - 0 => "spawn_stdio_stdin", - 1 => "spawn_stdio_stdout", - 2 => "spawn_stdio_stderr", - else => "spawn_stdio_memory_file", - }; - - // We use the linux syscall api because the glibc requirement is 2.27, which is a little close for comfort. - const rc = std.os.linux.memfd_create(label, 0); - - log("memfd_create({s}) = {d}", .{ label, rc }); - - switch (std.os.linux.getErrno(rc)) { - .SUCCESS => {}, - else => |errno| { - log("Failed to create memfd: {s}", .{@tagName(errno)}); - return; - }, - } - - const fd = bun.toFD(rc); - - var remain = this.byteSlice(); - - if (remain.len > 0) - // Hint at the size of the file - _ = bun.sys.ftruncate(fd, @intCast(remain.len)); - - // Dump all the bytes in there - var written: isize = 0; - while (remain.len > 0) { - switch (bun.sys.pwrite(fd, remain, written)) { - .err => |err| { - if (err.getErrno() == .AGAIN) { - continue; - } - - Output.debugWarn("Failed to write to memfd: {s}", .{@tagName(err.getErrno())}); - _ = bun.sys.close(fd); - return; - }, - .result => |result| { - if (result == 0) { - Output.debugWarn("Failed to write to memfd: EOF", .{}); - _ = bun.sys.close(fd); - return; - } - written += @intCast(result); - remain = remain[result..]; - }, - } - } - - switch (this.*) { - .array_buffer => this.array_buffer.deinit(), - .blob => this.blob.detach(), - else => {}, - } - - this.* = .{ .memfd = fd }; - } - - pub fn isPiped(self: Stdio) bool { - return switch (self) { - .array_buffer, .blob, .pipe => true, - else => false, - }; - } - - fn setUpChildIoPosixSpawn( - stdio: @This(), - actions: *PosixSpawn.Actions, - pipe_fd: [2]bun.FileDescriptor, - std_fileno: bun.FileDescriptor, - ) !void { - switch (stdio) { - .array_buffer, .blob, .pipe => { - std.debug.assert(!(stdio == .blob and stdio.blob.needsToReadFile())); - const idx: usize = if (std_fileno == bun.STDIN_FD) 0 else 1; - - try actions.dup2(bun.toFD(pipe_fd[idx]), std_fileno); - try actions.close(bun.toFD(pipe_fd[1 - idx])); - }, - .fd => |fd| { - try actions.dup2(fd, std_fileno); - }, - .memfd => |fd| { - try actions.dup2(fd, std_fileno); - }, - .path => |pathlike| { - const flag = if (std_fileno == bun.STDIN_FD) @as(u32, os.O.RDONLY) else @as(u32, std.os.O.WRONLY); - try actions.open(std_fileno, pathlike.slice(), flag | std.os.O.CREAT, 0o664); - }, - .inherit => { - if (comptime Environment.isMac) { - try actions.inherit(std_fileno); - } else { - try actions.dup2(std_fileno, std_fileno); - } - }, - .ignore => { - const flag = if (std_fileno == bun.STDIN_FD) @as(u32, os.O.RDONLY) else @as(u32, std.os.O.WRONLY); - try actions.openZ(std_fileno, "/dev/null", flag, 0o664); - }, - } - } - - fn setUpChildIoUvSpawn( - stdio: @This(), - std_fileno: i32, - pipe: *uv.uv_pipe_t, - isReadable: bool, - fd: bun.FileDescriptor, - ) !uv.uv_stdio_container_s { - return switch (stdio) { - .array_buffer, .blob, .pipe => { - if (uv.uv_pipe_init(uv.Loop.get(), pipe, 0) != 0) { - return error.FailedToCreatePipe; - } - if (fd != bun.invalid_fd) { - // we receive a FD so we open this into our pipe - if (uv.uv_pipe_open(pipe, bun.uvfdcast(fd)).errEnum()) |_| { - return error.FailedToCreatePipe; - } - return uv.uv_stdio_container_s{ - .flags = @intCast(uv.UV_INHERIT_STREAM), - .data = .{ .stream = @ptrCast(pipe) }, - }; - } - // we dont have any fd so we create a new pipe - return uv.uv_stdio_container_s{ - .flags = @intCast(uv.UV_CREATE_PIPE | if (isReadable) uv.UV_READABLE_PIPE else uv.UV_WRITABLE_PIPE), - .data = .{ .stream = @ptrCast(pipe) }, - }; - }, - .fd => |_fd| uv.uv_stdio_container_s{ - .flags = uv.UV_INHERIT_FD, - .data = .{ .fd = bun.uvfdcast(_fd) }, - }, - .path => |pathlike| uv.uv_stdio_container_s{ - .flags = uv.UV_INHERIT_FD, - .data = .{ .fd = bun.uvfdcast(bun.toLibUVOwnedFD((try std.fs.cwd().openFile(pathlike.slice(), .{})).handle)) }, - }, - .inherit => uv.uv_stdio_container_s{ - .flags = uv.UV_INHERIT_FD, - .data = .{ .fd = std_fileno }, - }, - .ignore => uv.uv_stdio_container_s{ - .flags = uv.UV_IGNORE, - .data = undefined, - }, - .memfd => unreachable, - }; - } - }; - - fn extractStdioBlob( - globalThis: *JSC.JSGlobalObject, - blob: JSC.WebCore.AnyBlob, - i: u32, - out_stdio: *Stdio, - ) bool { - const fd = bun.stdio(i); - - if (blob.needsToReadFile()) { - if (blob.store()) |store| { - if (store.data.file.pathlike == .fd) { - if (store.data.file.pathlike.fd == fd) { - out_stdio.* = Stdio{ .inherit = {} }; - } else { - switch (bun.FDTag.get(i)) { - .stdin => { - if (i == 1 or i == 2) { - globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); - return false; - } - }, - .stdout, .stderr => { - if (i == 0) { - globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); - return false; - } - }, - else => {}, - } - - out_stdio.* = Stdio{ .fd = store.data.file.pathlike.fd }; - } - - return true; - } - - out_stdio.* = .{ .path = store.data.file.pathlike.path }; - return true; - } - } - - out_stdio.* = .{ .blob = blob }; - return true; - } - - fn extractStdio( - globalThis: *JSC.JSGlobalObject, - i: u32, - value: JSValue, - out_stdio: *Stdio, - ) bool { - if (value.isEmptyOrUndefinedOrNull()) { - return true; - } - - if (value.isString()) { - const str = value.getZigString(globalThis); - if (str.eqlComptime("inherit")) { - out_stdio.* = Stdio{ .inherit = {} }; - } else if (str.eqlComptime("ignore")) { - out_stdio.* = Stdio{ .ignore = {} }; - } else if (str.eqlComptime("pipe")) { - out_stdio.* = Stdio{ .pipe = null }; - } else if (str.eqlComptime("ipc")) { - out_stdio.* = Stdio{ .pipe = null }; // TODO: - } else { - globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'pipe', 'ignore', Bun.file(pathOrFd), number, or null", .{}); - return false; - } - - return true; - } else if (value.isNumber()) { - const fd = value.asFileDescriptor(); - if (bun.uvfdcast(fd) < 0) { - globalThis.throwInvalidArguments("file descriptor must be a positive integer", .{}); - return false; - } - - switch (bun.FDTag.get(fd)) { - .stdin => { - if (i == 1 or i == 2) { - globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); - return false; - } - }, - - .stdout, .stderr => { - if (i == 0) { - globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); - return false; - } - }, - else => {}, - } - - out_stdio.* = Stdio{ .fd = fd }; - - return true; - } else if (value.as(JSC.WebCore.Blob)) |blob| { - return extractStdioBlob(globalThis, .{ .Blob = blob.dupe() }, i, out_stdio); - } else if (value.as(JSC.WebCore.Request)) |req| { - req.getBodyValue().toBlobIfPossible(); - return extractStdioBlob(globalThis, req.getBodyValue().useAsAnyBlob(), i, out_stdio); - } else if (value.as(JSC.WebCore.Response)) |req| { - req.getBodyValue().toBlobIfPossible(); - return extractStdioBlob(globalThis, req.getBodyValue().useAsAnyBlob(), i, out_stdio); - } else if (JSC.WebCore.ReadableStream.fromJS(value, globalThis)) |req_const| { - var req = req_const; - if (i == 0) { - if (req.toAnyBlob(globalThis)) |blob| { - return extractStdioBlob(globalThis, blob, i, out_stdio); - } - - switch (req.ptr) { - .File, .Blob => { - globalThis.throwTODO("Support fd/blob backed ReadableStream in spawn stdin. See https://github.com/oven-sh/bun/issues/8049"); - return false; - }, - .Direct, .JavaScript, .Bytes => { - if (req.isLocked(globalThis)) { - globalThis.throwInvalidArguments("ReadableStream cannot be locked", .{}); - return false; - } - - out_stdio.* = .{ .pipe = req }; - return true; - }, - .Invalid => { - globalThis.throwInvalidArguments("ReadableStream is in invalid state.", .{}); - return false; - }, - } - } - } else if (value.asArrayBuffer(globalThis)) |array_buffer| { - if (array_buffer.slice().len == 0) { - globalThis.throwInvalidArguments("ArrayBuffer cannot be empty", .{}); - return false; - } - - out_stdio.* = .{ - .array_buffer = JSC.ArrayBuffer.Strong{ - .array_buffer = array_buffer, - .held = JSC.Strong.create(array_buffer.value, globalThis), - }, - }; - - return true; - } - - globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'ignore', or null", .{}); - return false; - } pub fn handleIPCMessage( this: *Subprocess, @@ -3391,401 +2147,10 @@ pub const Subprocess = struct { } } - pub fn handleIPCClose(this: *Subprocess, _: IPC.Socket) void { - // uSocket is already freed so calling .close() on the socket can segfault + pub fn handleIPCClose(this: *Subprocess) void { this.ipc_mode = .none; this.updateHasPendingActivity(); } - pub fn pidfdFlagsForLinux() u32 { - const kernel = @import("../../../analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); - - // pidfd_nonblock only supported in 5.10+ - return if (kernel.orderWithoutTag(.{ .major = 5, .minor = 10, .patch = 0 }).compare(.gte)) - std.os.O.NONBLOCK - else - 0; - } - pub const IPCHandler = IPC.NewIPCHandler(Subprocess); - const ShellSubprocess = bun.shell.Subprocess; - const ShellSubprocessMini = bun.shell.SubprocessMini; - - // Machines which do not support pidfd_open (GVisor, Linux Kernel < 5.6) - // use a thread to wait for the child process to exit. - // We use a single thread to call waitpid() in a loop. - pub const WaiterThread = struct { - concurrent_queue: Queue = .{}, - lifecycle_script_concurrent_queue: LifecycleScriptTaskQueue = .{}, - queue: std.ArrayList(*Subprocess) = std.ArrayList(*Subprocess).init(bun.default_allocator), - shell: struct { - jsc: ShellSubprocessQueue = .{}, - mini: ShellSubprocessMiniQueue = .{}, - } = .{}, - lifecycle_script_queue: std.ArrayList(*LifecycleScriptSubprocess) = std.ArrayList(*LifecycleScriptSubprocess).init(bun.default_allocator), - started: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - signalfd: if (Environment.isLinux) bun.FileDescriptor else u0 = undefined, - eventfd: if (Environment.isLinux) bun.FileDescriptor else u0 = undefined, - - pub const ShellSubprocessQueue = NewShellQueue(ShellSubprocess); - pub const ShellSubprocessMiniQueue = NewShellQueue(ShellSubprocessMini); - - fn NewShellQueue(comptime T: type) type { - return struct { - queue: ConcurrentQueue = .{}, - active: std.ArrayList(*T) = std.ArrayList(*T).init(bun.default_allocator), - - pub const ShellTask = struct { - shell: *T, - next: ?*ShellTask = null, - - pub usingnamespace bun.New(@This()); - }; - pub const ConcurrentQueue = bun.UnboundedQueue(ShellTask, .next); - - pub const ResultTask = struct { - result: JSC.Maybe(PosixSpawn.WaitPidResult), - subprocess: *T, - - pub usingnamespace bun.New(@This()); - - pub const runFromJSThread = runFromMainThread; - - pub fn runFromMainThread(self: *@This()) void { - const result = self.result; - var subprocess = self.subprocess; - _ = subprocess.poll.wait_thread.ref_count.fetchSub(1, .Monotonic); - self.destroy(); - subprocess.onWaitPid(false, result); - } - - pub fn runFromMainThreadMini(self: *@This(), _: *void) void { - self.runFromMainThread(); - } - }; - - pub fn append(self: *@This(), shell: *T) void { - self.queue.push( - ShellTask.new(.{ - .shell = shell, - }), - ); - } - - pub fn loop(this: *@This()) void { - { - var batch = this.queue.popBatch(); - var iter = batch.iterator(); - this.active.ensureUnusedCapacity(batch.count) catch unreachable; - while (iter.next()) |task| { - this.active.appendAssumeCapacity(task.shell); - task.destroy(); - } - } - - var queue: []*T = this.active.items; - var i: usize = 0; - while (queue.len > 0 and i < queue.len) { - var process = queue[i]; - - // this case shouldn't really happen - if (process.pid == bun.invalid_fd.int()) { - _ = this.active.orderedRemove(i); - _ = process.poll.wait_thread.ref_count.fetchSub(1, .Monotonic); - queue = this.active.items; - continue; - } - - const result = PosixSpawn.wait4(process.pid, std.os.W.NOHANG, null); - if (result == .err or (result == .result and result.result.pid == process.pid)) { - _ = this.active.orderedRemove(i); - queue = this.active.items; - - T.GlobalHandle.init(process.globalThis).enqueueTaskConcurrentWaitPid(ResultTask.new(.{ - .result = result, - .subprocess = process, - })); - } - - i += 1; - } - } - }; - } - - pub fn setShouldUseWaiterThread() void { - @atomicStore(bool, &should_use_waiter_thread, true, .Monotonic); - } - - pub fn shouldUseWaiterThread() bool { - return @atomicLoad(bool, &should_use_waiter_thread, .Monotonic); - } - - pub const WaitTask = struct { - subprocess: *Subprocess, - next: ?*WaitTask = null, - }; - - pub fn appendShell(comptime Type: type, process: *Type) void { - const GlobalHandle = Type.GlobalHandle; - - if (process.poll == .wait_thread) { - process.poll.wait_thread.poll_ref.activate(GlobalHandle.init(process.globalThis).platformEventLoop()); - _ = process.poll.wait_thread.ref_count.fetchAdd(1, .Monotonic); - } else { - process.poll = .{ - .wait_thread = .{ - .poll_ref = .{}, - .ref_count = std.atomic.Value(u32).init(1), - }, - }; - process.poll.wait_thread.poll_ref.activate(GlobalHandle.init(process.globalThis).platformEventLoop()); - } - - switch (comptime Type) { - ShellSubprocess => instance.shell.jsc.append(process), - ShellSubprocessMini => instance.shell.mini.append(process), - else => @compileError("Unknown ShellSubprocess type"), - } - // if (comptime is_js) { - // process.updateHasPendingActivity(); - // } - - init() catch @panic("Failed to start WaiterThread"); - - if (comptime Environment.isLinux) { - const one = @as([8]u8, @bitCast(@as(usize, 1))); - _ = std.os.write(instance.eventfd.cast(), &one) catch @panic("Failed to write to eventfd"); - } - } - - pub const LifecycleScriptWaitTask = struct { - lifecycle_script_subprocess: *bun.install.LifecycleScriptSubprocess, - next: ?*LifecycleScriptWaitTask = null, - }; - - var should_use_waiter_thread = false; - - const stack_size = 512 * 1024; - pub const Queue = bun.UnboundedQueue(WaitTask, .next); - pub const LifecycleScriptTaskQueue = bun.UnboundedQueue(LifecycleScriptWaitTask, .next); - pub var instance: WaiterThread = .{}; - pub fn init() !void { - std.debug.assert(should_use_waiter_thread); - - if (instance.started.fetchMax(1, .Monotonic) > 0) { - return; - } - - var thread = try std.Thread.spawn(.{ .stack_size = stack_size }, loop, .{}); - thread.detach(); - - if (comptime Environment.isLinux) { - const linux = std.os.linux; - var mask = std.os.empty_sigset; - linux.sigaddset(&mask, std.os.SIG.CHLD); - instance.signalfd = bun.toFD(try std.os.signalfd(-1, &mask, linux.SFD.CLOEXEC | linux.SFD.NONBLOCK)); - instance.eventfd = bun.toFD(try std.os.eventfd(0, linux.EFD.NONBLOCK | linux.EFD.CLOEXEC | 0)); - } - } - - pub const WaitPidResultTask = struct { - result: JSC.Maybe(PosixSpawn.WaitPidResult), - rusage: Rusage, - subprocess: *Subprocess, - - pub fn runFromJSThread(self: *@This()) void { - const result = self.result; - var subprocess = self.subprocess; - _ = subprocess.poll.wait_thread.ref_count.fetchSub(1, .Monotonic); - bun.default_allocator.destroy(self); - subprocess.onWaitPid(false, subprocess.this_jsvalue, result, self.rusage); - } - }; - - pub fn append(process: *Subprocess) void { - if (process.poll == .wait_thread) { - process.poll.wait_thread.poll_ref.activate(process.globalThis.bunVM().event_loop_handle.?); - _ = process.poll.wait_thread.ref_count.fetchAdd(1, .Monotonic); - } else { - process.poll = .{ - .wait_thread = .{ - .poll_ref = .{}, - .ref_count = std.atomic.Value(u32).init(1), - }, - }; - process.poll.wait_thread.poll_ref.activate(process.globalThis.bunVM().event_loop_handle.?); - } - - const task = bun.default_allocator.create(WaitTask) catch unreachable; - task.* = WaitTask{ - .subprocess = process, - }; - instance.concurrent_queue.push(task); - process.updateHasPendingActivity(); - - init() catch @panic("Failed to start WaiterThread"); - - if (comptime Environment.isLinux) { - const one = @as([8]u8, @bitCast(@as(usize, 1))); - _ = std.os.write(instance.eventfd.cast(), &one) catch @panic("Failed to write to eventfd"); - } - } - - pub fn appendLifecycleScriptSubprocess(lifecycle_script: *LifecycleScriptSubprocess) void { - const task = bun.default_allocator.create(LifecycleScriptWaitTask) catch unreachable; - task.* = LifecycleScriptWaitTask{ - .lifecycle_script_subprocess = lifecycle_script, - }; - instance.lifecycle_script_concurrent_queue.push(task); - - init() catch @panic("Failed to start WaiterThread"); - - if (comptime Environment.isLinux) { - const one = @as([8]u8, @bitCast(@as(usize, 1))); - _ = std.os.write(instance.eventfd.cast(), &one) catch @panic("Failed to write to eventfd"); - } - } - - fn loopSubprocess(this: *WaiterThread) void { - { - var batch = this.concurrent_queue.popBatch(); - var iter = batch.iterator(); - this.queue.ensureUnusedCapacity(batch.count) catch unreachable; - while (iter.next()) |task| { - this.queue.appendAssumeCapacity(task.subprocess); - bun.default_allocator.destroy(task); - } - } - - var queue: []*Subprocess = this.queue.items; - var i: usize = 0; - while (queue.len > 0 and i < queue.len) { - var process = queue[i]; - - // this case shouldn't really happen - if (process.pid == bun.invalid_fd.int()) { - _ = this.queue.orderedRemove(i); - _ = process.poll.wait_thread.ref_count.fetchSub(1, .Monotonic); - queue = this.queue.items; - continue; - } - - var rusage_result: Rusage = std.mem.zeroes(Rusage); - - const result = PosixSpawn.wait4(process.pid, std.os.W.NOHANG, &rusage_result); - if (result == .err or (result == .result and result.result.pid == process.pid)) { - _ = this.queue.orderedRemove(i); - process.pid_rusage = rusage_result; - queue = this.queue.items; - - const task = bun.default_allocator.create(WaitPidResultTask) catch unreachable; - task.* = WaitPidResultTask{ - .result = result, - .subprocess = process, - .rusage = rusage_result, - }; - - process.globalThis.bunVMConcurrently().enqueueTaskConcurrent( - JSC.ConcurrentTask.create( - JSC.Task.init(task), - ), - ); - } - - i += 1; - } - } - - fn loopLifecycleScriptsSubprocess(this: *WaiterThread) void { - { - var batch = this.lifecycle_script_concurrent_queue.popBatch(); - var iter = batch.iterator(); - this.lifecycle_script_queue.ensureUnusedCapacity(batch.count) catch unreachable; - while (iter.next()) |task| { - this.lifecycle_script_queue.appendAssumeCapacity(task.lifecycle_script_subprocess); - bun.default_allocator.destroy(task); - } - } - - var queue: []*LifecycleScriptSubprocess = this.lifecycle_script_queue.items; - var i: usize = 0; - while (queue.len > 0 and i < queue.len) { - var lifecycle_script_subprocess = queue[i]; - - if (lifecycle_script_subprocess.pid == bun.invalid_fd.int()) { - _ = this.lifecycle_script_queue.orderedRemove(i); - queue = this.lifecycle_script_queue.items; - } - - // const result = PosixSpawn.waitpid(lifecycle_script_subprocess.pid, std.os.W.NOHANG); - switch (PosixSpawn.waitpid(lifecycle_script_subprocess.pid, std.os.W.NOHANG)) { - .err => |err| { - std.debug.print("waitpid error: {s}\n", .{@tagName(err.getErrno())}); - Output.prettyErrorln("error: Failed to run {s} script from \"{s}\" due to error {d} {s}", .{ - lifecycle_script_subprocess.scriptName(), - lifecycle_script_subprocess.package_name, - err.errno, - @tagName(err.getErrno()), - }); - Output.flush(); - _ = lifecycle_script_subprocess.manager.pending_lifecycle_script_tasks.fetchSub(1, .Monotonic); - _ = LifecycleScriptSubprocess.alive_count.fetchSub(1, .Monotonic); - }, - .result => |result| { - if (result.pid == lifecycle_script_subprocess.pid) { - _ = this.lifecycle_script_queue.orderedRemove(i); - queue = this.lifecycle_script_queue.items; - - lifecycle_script_subprocess.onResult(.{ - .pid = result.pid, - .status = result.status, - }); - } - }, - } - - i += 1; - } - } - - pub fn loop() void { - Output.Source.configureNamedThread("Waitpid"); - - var this = &instance; - - while (true) { - this.loopSubprocess(); - this.loopLifecycleScriptsSubprocess(); - this.shell.jsc.loop(); - this.shell.mini.loop(); - - if (comptime Environment.isLinux) { - var polls = [_]std.os.pollfd{ - .{ - .fd = this.signalfd.cast(), - .events = std.os.POLL.IN | std.os.POLL.ERR, - .revents = 0, - }, - .{ - .fd = this.eventfd.cast(), - .events = std.os.POLL.IN | std.os.POLL.ERR, - .revents = 0, - }, - }; - - _ = std.os.poll(&polls, std.math.maxInt(i32)) catch 0; - - // Make sure we consume any pending signals - var buf: [1024]u8 = undefined; - _ = std.os.read(this.signalfd.cast(), &buf) catch 0; - } else { - var mask = std.os.empty_sigset; - var signal: c_int = std.os.SIG.CHLD; - const rc = std.c.sigwait(&mask, &signal); - _ = rc; - } - } - } - }; }; diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig index 1a143461b849de..8c1ba7d42e0c6f 100644 --- a/src/bun.js/api/html_rewriter.zig +++ b/src/bun.js/api/html_rewriter.zig @@ -469,13 +469,6 @@ pub const HTMLRewriter = struct { }; return err.toErrorInstance(sink.global); }, - error.InvalidStream => { - var err = JSC.SystemError{ - .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), - .message = bun.String.static("Invalid stream"), - }; - return err.toErrorInstance(sink.global); - }, else => { var err = JSC.SystemError{ .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_STREAM_CANNOT_PIPE))), @@ -505,6 +498,7 @@ pub const HTMLRewriter = struct { if (sink.response.body.value == .Locked and @intFromPtr(sink.response.body.value.Locked.task) == @intFromPtr(sink) and sink.response.body.value.Locked.promise == null) { + sink.response.body.value.Locked.readable.deinit(); sink.response.body.value = .{ .Empty = {} }; // is there a pending promise? // we will need to reject it diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 04e7fe9ab7c888..7e25effb849b15 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1407,6 +1407,12 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp ctx.flags.has_written_status = true; ctx.end("", ctx.shouldCloseConnection()); } else { + // avoid writing the status again and missmatching the content-length + if (ctx.flags.has_written_status) { + ctx.end("", ctx.shouldCloseConnection()); + return; + } + if (ctx.flags.is_web_browser_navigation) { resp.writeStatus("200 OK"); ctx.flags.has_written_status = true; @@ -1417,11 +1423,12 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp ctx.end(welcome_page_html_gz, ctx.shouldCloseConnection()); return; } - - if (!ctx.flags.has_written_status) - resp.writeStatus("200 OK"); + const missing_content = "Welcome to Bun! To get started, return a Response object."; + resp.writeStatus("200 OK"); + resp.writeHeader("content-type", MimeType.text.value); + resp.writeHeaderInt("content-length", missing_content.len); ctx.flags.has_written_status = true; - ctx.end("Welcome to Bun! To get started, return a Response object.", ctx.shouldCloseConnection()); + ctx.end(missing_content, ctx.shouldCloseConnection()); } } } @@ -1685,9 +1692,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // the promise is pending if (body.value.Locked.action != .none or body.value.Locked.promise != null) { this.pending_promises_for_abort += 1; - } else if (body.value.Locked.readable != null) { - body.value.Locked.readable.?.abort(this.server.globalThis); - body.value.Locked.readable = null; + } else if (body.value.Locked.readable.get()) |readable| { + readable.abort(this.server.globalThis); + body.value.Locked.readable.deinit(); any_js_calls = true; } body.value.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); @@ -1696,8 +1703,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (this.response_ptr) |response| { if (response.body.value == .Locked) { - if (response.body.value.Locked.readable) |*readable| { - response.body.value.Locked.readable = null; + if (response.body.value.Locked.readable.get()) |readable| { + defer response.body.value.Locked.readable.deinit(); readable.abort(this.server.globalThis); any_js_calls = true; } @@ -1774,7 +1781,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp ctxLog("finalizeWithoutDeinit: stream != null", .{}); this.byte_stream = null; - stream.unpipe(); + stream.unpipeWithoutDeref(); } this.readable_stream_ref.deinit(); @@ -1800,6 +1807,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } pub fn deinit(this: *RequestContext) void { + if (!this.isDeadRequest()) { + ctxLog("deinit ({*}) waiting request", .{this}); + return; + } if (this.defer_deinit_until_callback_completes) |defer_deinit| { defer_deinit.* = true; ctxLog("deferred deinit ({*})", .{this}); @@ -2037,7 +2048,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } if (Environment.isLinux) { - if (!(bun.isRegularFile(stat.mode) or std.os.S.ISFIFO(stat.mode))) { + if (!(bun.isRegularFile(stat.mode) or std.os.S.ISFIFO(stat.mode) or std.os.S.ISSOCK(stat.mode))) { if (auto_close) { _ = bun.sys.close(fd); } @@ -2145,11 +2156,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (this.blob == .Blob) { const original_size = this.blob.Blob.size; - + // if we dont know the size we use the stat size this.blob.Blob.size = if (original_size == 0 or original_size == Blob.max_size) stat_size - else - @min(original_size, stat_size); + else // the blob can be a slice of a file + @max(original_size, stat_size); } if (!this.flags.has_written_status) @@ -2202,7 +2213,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp var this = pair.this; var stream = pair.stream; if (this.resp == null or this.flags.aborted) { - stream.value.unprotect(); + stream.cancel(this.server.globalThis); + this.readable_stream_ref.deinit(); this.finalizeForAbort(); return; } @@ -2268,9 +2280,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp response_stream.detach(); this.sink = null; response_stream.sink.destroy(); + stream.done(this.server.globalThis); + this.readable_stream_ref.deinit(); this.endStream(this.shouldCloseConnection()); this.finalize(); - stream.value.unprotect(); return; } @@ -2295,7 +2308,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.pending_promises_for_abort += 1; this.response_ptr.?.body.value = .{ .Locked = .{ - .readable = stream, + .readable = JSC.WebCore.ReadableStream.Strong.init(stream, globalThis), .global = globalThis, }, }; @@ -2310,14 +2323,19 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp }, .Fulfilled => { streamLog("promise Fulfilled", .{}); - defer stream.value.unprotect(); + defer { + stream.done(globalThis); + this.readable_stream_ref.deinit(); + } this.handleResolveStream(); }, .Rejected => { streamLog("promise Rejected", .{}); - defer stream.value.unprotect(); - + defer { + stream.cancel(globalThis); + this.readable_stream_ref.deinit(); + } this.handleRejectStream(globalThis, promise.result(globalThis.vm())); }, } @@ -2336,7 +2354,8 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (this.flags.aborted) { response_stream.detach(); stream.cancel(globalThis); - defer stream.value.unprotect(); + defer this.readable_stream_ref.deinit(); + response_stream.sink.markDone(); this.finalizeForAbort(); response_stream.sink.onFirstWrite = null; @@ -2345,8 +2364,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; } - stream.value.ensureStillAlive(); - defer stream.value.unprotect(); + defer this.readable_stream_ref.deinit(); const is_in_progress = response_stream.sink.has_backpressure or !(response_stream.sink.wrote == 0 and response_stream.sink.buffer.len == 0); @@ -2563,7 +2581,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (req.response_ptr) |resp| { if (resp.body.value == .Locked) { - resp.body.value.Locked.readable.?.done(); + if (resp.body.value.Locked.readable.get()) |stream| { + stream.done(req.server.globalThis); + } + resp.body.value.Locked.readable.deinit(); resp.body.value = .{ .Used = {} }; } } @@ -2608,7 +2629,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } pub fn handleRejectStream(req: *@This(), globalThis: *JSC.JSGlobalObject, err: JSValue) void { - _ = globalThis; streamLog("handleRejectStream", .{}); if (req.sink) |wrapper| { @@ -2623,7 +2643,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp if (req.response_ptr) |resp| { if (resp.body.value == .Locked) { - resp.body.value.Locked.readable.?.done(); + if (resp.body.value.Locked.readable.get()) |stream| { + stream.done(globalThis); + } + resp.body.value.Locked.readable.deinit(); resp.body.value = .{ .Used = {} }; } } @@ -2685,10 +2708,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp return; } - if (lock.readable) |stream_| { + if (lock.readable.get()) |stream_| { const stream: JSC.WebCore.ReadableStream = stream_; - stream.value.ensureStillAlive(); - + // we hold the stream alive until we're done with it + this.readable_stream_ref = lock.readable; value.* = .{ .Used = {} }; if (stream.isLocked(this.server.globalThis)) { @@ -2703,8 +2726,9 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } switch (stream.ptr) { - .Invalid => {}, - + .Invalid => { + this.readable_stream_ref.deinit(); + }, // toBlobIfPossible should've caught this .Blob, .File => unreachable, @@ -2719,10 +2743,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp .Bytes => |byte_stream| { std.debug.assert(byte_stream.pipe.ctx == null); std.debug.assert(this.byte_stream == null); - if (this.resp == null) { // we don't have a response, so we can discard the stream - stream.detachIfPossible(this.server.globalThis); + stream.done(this.server.globalThis); + this.readable_stream_ref.deinit(); return; } const resp = this.resp.?; @@ -2730,20 +2754,13 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // we can avoid streaming it and just send it all at once. if (byte_stream.has_received_last_chunk) { this.blob.from(byte_stream.buffer); + this.readable_stream_ref.deinit(); this.doRenderBlob(); - // is safe to detach here because we're not going to receive any more data - stream.detachIfPossible(this.server.globalThis); return; } byte_stream.pipe = JSC.WebCore.Pipe.New(@This(), onPipe).init(this); - this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(stream, this.server.globalThis) catch { - // Invalid Stream - this.renderMissing(); - return; - }; - // we now hold a reference so we can safely ask to detach and will be detached when the last ref is dropped - stream.detachIfPossible(this.server.globalThis); + this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(stream, this.server.globalThis); this.byte_stream = byte_stream; this.response_buf_owned = byte_stream.buffer.moveToUnmanaged(); @@ -2769,7 +2786,6 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp // someone else is waiting for the stream or waiting for `onStartStreaming` const readable = value.toReadableStream(this.server.globalThis); readable.ensureStillAlive(); - readable.protect(); this.doRenderWithBody(value); return; } @@ -3206,7 +3222,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp var body = this.request_body.?; if (body.value == .Locked) { - if (body.value.Locked.readable) |readable| { + if (body.value.Locked.readable.get()) |readable| { if (readable.ptr == .Bytes) { std.debug.assert(this.request_body_buf.items.len == 0); var vm = this.server.vm; @@ -3221,6 +3237,11 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp bun.default_allocator, ); } else { + var prev = body.value.Locked.readable; + body.value.Locked.readable = .{}; + readable.value.ensureStillAlive(); + prev.deinit(); + readable.value.ensureStillAlive(); readable.ptr.Bytes.onData( .{ .temporary_and_done = bun.ByteList.initConst(chunk), @@ -3235,7 +3256,7 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp } if (last) { - var bytes = this.request_body_buf; + var bytes = &this.request_body_buf; var old = body.value; @@ -3270,8 +3291,10 @@ fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comp this.request_body_buf = .{}; if (old == .Locked) { - var vm = this.server.vm; - defer vm.drainMicrotasks(); + var loop = this.server.vm.eventLoop(); + loop.enter(); + defer loop.exit(); + old.resolve(&body.value, this.server.globalThis); } return; diff --git a/src/bun.js/api/streams.classes.ts b/src/bun.js/api/streams.classes.ts new file mode 100644 index 00000000000000..707a03e2ea02e3 --- /dev/null +++ b/src/bun.js/api/streams.classes.ts @@ -0,0 +1,50 @@ +import { define } from "../../codegen/class-definitions"; + +function source(name) { + return define({ + name: name + "InternalReadableStreamSource", + construct: false, + noConstructor: true, + finalize: true, + configurable: false, + proto: { + drain: { + fn: "drainFromJS", + length: 1, + }, + start: { + fn: "startFromJS", + length: 1, + }, + updateRef: { + fn: "updateRefFromJS", + length: 1, + }, + onClose: { + getter: "getOnCloseFromJS", + setter: "setOnCloseFromJS", + }, + onDrain: { + getter: "getOnDrainFromJS", + setter: "setOnDrainFromJS", + }, + cancel: { + fn: "cancelFromJS", + length: 1, + }, + pull: { + fn: "pullFromJS", + length: 1, + }, + isClosed: { + getter: "getIsClosedFromJS", + }, + }, + klass: {}, + values: ["pendingPromise", "onCloseCallback", "onDrainCallback"], + }); +} + +const sources = ["Blob", "File", "Bytes"]; + +export default sources.map(source); diff --git a/src/bun.js/api/streams.classes.zig b/src/bun.js/api/streams.classes.zig new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/src/bun.js/bindings/BunDebugger.cpp b/src/bun.js/bindings/BunDebugger.cpp index 9cad0c692e0e9e..42c3058a13ca95 100644 --- a/src/bun.js/bindings/BunDebugger.cpp +++ b/src/bun.js/bindings/BunDebugger.cpp @@ -1,5 +1,7 @@ #include "root.h" +#include "ZigGlobalObject.h" + #include #include #include diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 82269787231764..4f35a4df58388b 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "ScriptExecutionContext.h" #include "headers-handwritten.h" #include "node_api.h" #include "ZigGlobalObject.h" @@ -599,82 +600,90 @@ static void loadSignalNumberMap() std::call_once(signalNameToNumberMapOnceFlag, [] { signalNameToNumberMap = new HashMap(); signalNameToNumberMap->reserveInitialCapacity(31); - signalNameToNumberMap->add(signalNames[0], SIGHUP); +#if OS(WINDOWS) + // libuv supported signals signalNameToNumberMap->add(signalNames[1], SIGINT); signalNameToNumberMap->add(signalNames[2], SIGQUIT); - signalNameToNumberMap->add(signalNames[3], SIGILL); + signalNameToNumberMap->add(signalNames[9], SIGKILL); + signalNameToNumberMap->add(signalNames[15], SIGTERM); +#else + signalNameToNumberMap->add(signalNames[0], SIGHUP); + signalNameToNumberMap->add(signalNames[1], SIGINT); + signalNameToNumberMap->add(signalNames[2], SIGQUIT); + signalNameToNumberMap->add(signalNames[3], SIGILL); #ifdef SIGTRAP - signalNameToNumberMap->add(signalNames[4], SIGTRAP); + signalNameToNumberMap->add(signalNames[4], SIGTRAP); #endif - signalNameToNumberMap->add(signalNames[5], SIGABRT); + signalNameToNumberMap->add(signalNames[5], SIGABRT); #ifdef SIGIOT - signalNameToNumberMap->add(signalNames[6], SIGIOT); + signalNameToNumberMap->add(signalNames[6], SIGIOT); #endif #ifdef SIGBUS - signalNameToNumberMap->add(signalNames[7], SIGBUS); + signalNameToNumberMap->add(signalNames[7], SIGBUS); #endif - signalNameToNumberMap->add(signalNames[8], SIGFPE); - signalNameToNumberMap->add(signalNames[9], SIGKILL); + signalNameToNumberMap->add(signalNames[8], SIGFPE); + signalNameToNumberMap->add(signalNames[9], SIGKILL); #ifdef SIGUSR1 - signalNameToNumberMap->add(signalNames[10], SIGUSR1); + signalNameToNumberMap->add(signalNames[10], SIGUSR1); #endif - signalNameToNumberMap->add(signalNames[11], SIGSEGV); + signalNameToNumberMap->add(signalNames[11], SIGSEGV); #ifdef SIGUSR2 - signalNameToNumberMap->add(signalNames[12], SIGUSR2); + signalNameToNumberMap->add(signalNames[12], SIGUSR2); #endif #ifdef SIGPIPE - signalNameToNumberMap->add(signalNames[13], SIGPIPE); + signalNameToNumberMap->add(signalNames[13], SIGPIPE); #endif #ifdef SIGALRM - signalNameToNumberMap->add(signalNames[14], SIGALRM); + signalNameToNumberMap->add(signalNames[14], SIGALRM); #endif - signalNameToNumberMap->add(signalNames[15], SIGTERM); + signalNameToNumberMap->add(signalNames[15], SIGTERM); #ifdef SIGCHLD - signalNameToNumberMap->add(signalNames[16], SIGCHLD); + signalNameToNumberMap->add(signalNames[16], SIGCHLD); #endif #ifdef SIGCONT - signalNameToNumberMap->add(signalNames[17], SIGCONT); + signalNameToNumberMap->add(signalNames[17], SIGCONT); #endif #ifdef SIGSTOP - signalNameToNumberMap->add(signalNames[18], SIGSTOP); + signalNameToNumberMap->add(signalNames[18], SIGSTOP); #endif #ifdef SIGTSTP - signalNameToNumberMap->add(signalNames[19], SIGTSTP); + signalNameToNumberMap->add(signalNames[19], SIGTSTP); #endif #ifdef SIGTTIN - signalNameToNumberMap->add(signalNames[20], SIGTTIN); + signalNameToNumberMap->add(signalNames[20], SIGTTIN); #endif #ifdef SIGTTOU - signalNameToNumberMap->add(signalNames[21], SIGTTOU); + signalNameToNumberMap->add(signalNames[21], SIGTTOU); #endif #ifdef SIGURG - signalNameToNumberMap->add(signalNames[22], SIGURG); + signalNameToNumberMap->add(signalNames[22], SIGURG); #endif #ifdef SIGXCPU - signalNameToNumberMap->add(signalNames[23], SIGXCPU); + signalNameToNumberMap->add(signalNames[23], SIGXCPU); #endif #ifdef SIGXFSZ - signalNameToNumberMap->add(signalNames[24], SIGXFSZ); + signalNameToNumberMap->add(signalNames[24], SIGXFSZ); #endif #ifdef SIGVTALRM - signalNameToNumberMap->add(signalNames[25], SIGVTALRM); + signalNameToNumberMap->add(signalNames[25], SIGVTALRM); #endif #ifdef SIGPROF - signalNameToNumberMap->add(signalNames[26], SIGPROF); + signalNameToNumberMap->add(signalNames[26], SIGPROF); #endif - signalNameToNumberMap->add(signalNames[27], SIGWINCH); + signalNameToNumberMap->add(signalNames[27], SIGWINCH); #ifdef SIGIO - signalNameToNumberMap->add(signalNames[28], SIGIO); + signalNameToNumberMap->add(signalNames[28], SIGIO); #endif #ifdef SIGINFO - signalNameToNumberMap->add(signalNames[29], SIGINFO); + signalNameToNumberMap->add(signalNames[29], SIGINFO); #endif #ifndef SIGINFO - signalNameToNumberMap->add(signalNames[29], 255); + signalNameToNumberMap->add(signalNames[29], 255); #endif #ifdef SIGSYS - signalNameToNumberMap->add(signalNames[30], SIGSYS); + signalNameToNumberMap->add(signalNames[30], SIGSYS); +#endif #endif }); } @@ -699,22 +708,25 @@ void signalHandler(uv_signal_t* signal, int signalNumber) if (UNLIKELY(!context)) return; - JSGlobalObject* lexicalGlobalObject = context->jsGlobalObject(); - Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); - - Process* process = jsCast(globalObject->processObject()); - - String signalName = signalNumberToNameMap->get(signalNumber); - Identifier signalNameIdentifier = Identifier::fromString(globalObject->vm(), signalName); - MarkedArgumentBuffer args; - args.append(jsNumber(signalNumber)); - // TODO(@paperdave): add an ASSERT(isMainThread()); - // This should be true on posix if I understand sigaction right - // On Windows it should be true if the uv_signal is created on the main thread's loop - // - // I would like to assert this because if that assumption is not true, - // this call will probably cause very confusing bugs. - process->wrapped().emitForBindings(signalNameIdentifier, args); + // signal handlers can be run on any thread + context->postTaskConcurrently([signalNumber](ScriptExecutionContext& context) { + JSGlobalObject* lexicalGlobalObject = context.jsGlobalObject(); + Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); + + Process* process = jsCast(globalObject->processObject()); + + String signalName = signalNumberToNameMap->get(signalNumber); + Identifier signalNameIdentifier = Identifier::fromString(globalObject->vm(), signalName); + MarkedArgumentBuffer args; + args.append(jsNumber(signalNumber)); + // TODO(@paperdave): add an ASSERT(isMainThread()); + // This should be true on posix if I understand sigaction right + // On Windows it should be true if the uv_signal is created on the main thread's loop + // + // I would like to assert this because if that assumption is not true, + // this call will probably cause very confusing bugs. + process->wrapped().emitForBindings(signalNameIdentifier, args); + }); }; static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& eventName, bool isAdded) @@ -2576,9 +2588,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, } JSC::JSValue signalValue = callFrame->argument(1); -#if !OS(WINDOWS) int signal = SIGTERM; - if (signalValue.isNumber()) { signal = signalValue.toInt32(globalObject); RETURN_IF_EXCEPTION(scope, {}); @@ -2598,21 +2608,10 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, return JSValue::encode(jsUndefined()); } - int result = kill(pid, signal); -#else - int signal = 1; - if (signalValue.isNumber()) { - signal = signalValue.toInt32(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - } else if (signalValue.isString()) { - throwTypeError(globalObject, scope, "TODO: implement this function with strings on Windows! Sorry!!"_s); - RETURN_IF_EXCEPTION(scope, {}); - } else if (!signalValue.isUndefinedOrNull()) { - throwTypeError(globalObject, scope, "signal must be a string or number"_s); - return JSValue::encode(jsUndefined()); - } - +#if OS(WINDOWS) int result = uv_kill(pid, signal); +#else + int result = kill(pid, signal); #endif if (result < 0) { diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index cf9160dad0050d..84a7ae86889eb4 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -1,14 +1,19 @@ #include "root.h" #include "headers-handwritten.h" #include -#include "helpers.h" #include "simdutf.h" #include "JSDOMURL.h" #include "DOMURL.h" #include "ZigGlobalObject.h" #include "IDLTypes.h" -#include "JSDOMWrapperCache.h" +#include +#include +#include +#include +#include + +#include "JSDOMWrapperCache.h" #include "JSDOMAttribute.h" #include "JSDOMBinding.h" #include "JSDOMConstructor.h" @@ -21,12 +26,7 @@ #include "JSDOMGlobalObjectInlines.h" #include "JSDOMOperation.h" -#include -#include #include "GCDefferalContext.h" -#include -#include -#include extern "C" void mi_free(void* ptr); diff --git a/src/bun.js/bindings/JSDOMGlobalObject.h b/src/bun.js/bindings/JSDOMGlobalObject.h index 03a8569e0c9d2f..9e33508a935bf5 100644 --- a/src/bun.js/bindings/JSDOMGlobalObject.h +++ b/src/bun.js/bindings/JSDOMGlobalObject.h @@ -2,6 +2,10 @@ #include "root.h" +namespace Zig { +class GlobalObject; +} + #include "DOMWrapperWorld.h" #include diff --git a/src/bun.js/bindings/JSDOMWrapper.h b/src/bun.js/bindings/JSDOMWrapper.h index 5d24fbc7fabe5f..4698f86d856ba6 100644 --- a/src/bun.js/bindings/JSDOMWrapper.h +++ b/src/bun.js/bindings/JSDOMWrapper.h @@ -21,9 +21,9 @@ #pragma once #include "root.h" +#include "ZigGlobalObject.h" #include "JSDOMGlobalObject.h" -#include "ZigGlobalObject.h" #include "NodeConstants.h" #include #include diff --git a/src/bun.js/bindings/KeyObject.cpp b/src/bun.js/bindings/KeyObject.cpp index bff4fc1e8133cf..f90c17c7d31534 100644 --- a/src/bun.js/bindings/KeyObject.cpp +++ b/src/bun.js/bindings/KeyObject.cpp @@ -470,6 +470,7 @@ JSC::EncodedJSValue KeyObject__createPrivateKey(JSC::JSGlobalObject* globalObjec RETURN_IF_EXCEPTION(scope, encodedJSValue()); if (format == "pem"_s) { + ASSERT(data); auto bio = BIOPtr(BIO_new_mem_buf(const_cast((char*)data), byteLength)); auto pkey = EvpPKeyPtr(PEM_read_bio_PrivateKey(bio.get(), nullptr, PasswordCallback, &passphrase)); diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index 3259649c878508..316e7c15ec1ba7 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -310,4 +310,32 @@ ScriptExecutionContext* executionContext(JSC::JSGlobalObject* globalObject) return JSC::jsCast(globalObject)->scriptExecutionContext(); } +void ScriptExecutionContext::postTaskConcurrently(Function&& lambda) +{ + auto* task = new EventLoopTask(WTFMove(lambda)); + reinterpret_cast(m_globalObject)->queueTaskConcurrently(task); +} +// Executes the task on context's thread asynchronously. +void ScriptExecutionContext::postTask(Function&& lambda) +{ + auto* task = new EventLoopTask(WTFMove(lambda)); + reinterpret_cast(m_globalObject)->queueTask(task); +} +// Executes the task on context's thread asynchronously. +void ScriptExecutionContext::postTask(EventLoopTask* task) +{ + reinterpret_cast(m_globalObject)->queueTask(task); +} +// Executes the task on context's thread asynchronously. +void ScriptExecutionContext::postTaskOnTimeout(EventLoopTask* task, Seconds timeout) +{ + reinterpret_cast(m_globalObject)->queueTaskOnTimeout(task, static_cast(timeout.milliseconds())); +} +// Executes the task on context's thread asynchronously. +void ScriptExecutionContext::postTaskOnTimeout(Function&& lambda, Seconds timeout) +{ + auto* task = new EventLoopTask(WTFMove(lambda)); + postTaskOnTimeout(task, timeout); +} + } diff --git a/src/bun.js/bindings/ScriptExecutionContext.h b/src/bun.js/bindings/ScriptExecutionContext.h index cc94ecae774a41..4b603235941339 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.h +++ b/src/bun.js/bindings/ScriptExecutionContext.h @@ -19,10 +19,6 @@ template struct WebSocketContext; } -#ifndef ZIG_GLOBAL_OBJECT_DEFINED -#include "ZigGlobalObject.h" -#endif - struct us_socket_t; struct us_socket_context_t; struct us_loop_t; @@ -169,33 +165,15 @@ class ScriptExecutionContext : public CanMakeWeakPtr { void addToContextsMap(); void removeFromContextsMap(); - void postTaskConcurrently(Function&& lambda) - { - auto* task = new EventLoopTask(WTFMove(lambda)); - reinterpret_cast(m_globalObject)->queueTaskConcurrently(task); - } + void postTaskConcurrently(Function&& lambda); // Executes the task on context's thread asynchronously. - void postTask(Function&& lambda) - { - auto* task = new EventLoopTask(WTFMove(lambda)); - reinterpret_cast(m_globalObject)->queueTask(task); - } + void postTask(Function&& lambda); // Executes the task on context's thread asynchronously. - void postTask(EventLoopTask* task) - { - reinterpret_cast(m_globalObject)->queueTask(task); - } + void postTask(EventLoopTask* task); // Executes the task on context's thread asynchronously. - void postTaskOnTimeout(EventLoopTask* task, Seconds timeout) - { - reinterpret_cast(m_globalObject)->queueTaskOnTimeout(task, static_cast(timeout.milliseconds())); - } + void postTaskOnTimeout(EventLoopTask* task, Seconds timeout); // Executes the task on context's thread asynchronously. - void postTaskOnTimeout(Function&& lambda, Seconds timeout) - { - auto* task = new EventLoopTask(WTFMove(lambda)); - postTaskOnTimeout(task, timeout); - } + void postTaskOnTimeout(Function&& lambda, Seconds timeout); template void postCrossThreadTask(Arguments&&... arguments) diff --git a/src/bun.js/bindings/Sink.h b/src/bun.js/bindings/Sink.h index c7d56354d9021c..6f7168c004ee3f 100644 --- a/src/bun.js/bindings/Sink.h +++ b/src/bun.js/bindings/Sink.h @@ -9,7 +9,6 @@ enum SinkID : uint8_t { HTMLRewriterSink = 3, HTTPResponseSink = 4, HTTPSResponseSink = 5, - UVStreamSink = 6, }; static constexpr unsigned numberOfSinkIDs diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 4ea39198ab6bd7..b8910c0c6cc06f 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -4,6 +4,8 @@ #include #include "helpers.h" #include "BunClientData.h" +#include "JavaScriptCore/JSCJSValue.h" +#include "JavaScriptCore/AggregateError.h" #include "JavaScriptCore/JSObjectInlines.h" #include "JavaScriptCore/InternalFieldTuple.h" #include "JavaScriptCore/BytecodeIndex.h" @@ -58,6 +60,7 @@ #include "JavaScriptCore/StackVisitor.h" #include "JavaScriptCore/VM.h" #include "JavaScriptCore/WasmFaultSignalHandler.h" +#include "wtf/Assertions.h" #include "wtf/Gigacage.h" #include "wtf/URL.h" #include "wtf/URLParser.h" @@ -1941,30 +1944,10 @@ JSC_DEFINE_HOST_FUNCTION(functionLazyLoad, default: { JSC::JSValue moduleName = callFrame->argument(0); if (moduleName.isNumber()) { - switch (moduleName.toInt32(globalObject)) { - case 0: { - JSC::throwTypeError(globalObject, scope, "$lazy expects a string"_s); - scope.release(); - return JSC::JSValue::encode(JSC::JSValue {}); - } - - case ReadableStreamTag::Blob: { - return ByteBlob__JSReadableStreamSource__load(globalObject); - } - case ReadableStreamTag::File: { - return FileReader__JSReadableStreamSource__load(globalObject); - } - case ReadableStreamTag::Bytes: { - return ByteStream__JSReadableStreamSource__load(globalObject); - } - - default: { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::throwTypeError(globalObject, scope, "$lazy expects a string"_s); - scope.release(); - return JSC::JSValue::encode(JSC::JSValue {}); - } - } + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSC::throwTypeError(globalObject, scope, "$lazy expects a string"_s); + scope.release(); + return JSC::JSValue::encode(JSC::JSValue {}); } auto string = moduleName.toWTFString(globalObject); @@ -2214,6 +2197,8 @@ JSC_DEFINE_HOST_FUNCTION(functionLazyLoad, return JSC::JSValue::encode(JSC::jsUndefined()); #endif } + // silence warning + RELEASE_ASSERT_NOT_REACHED(); } } @@ -2437,17 +2422,18 @@ extern "C" void ReadableStream__cancel(JSC__JSValue possibleReadableStream, Zig: ReadableStream::cancel(*globalObject, readableStream, exception); } -extern "C" void ReadableStream__detach(JSC__JSValue possibleReadableStream, Zig::GlobalObject* globalObject); extern "C" void ReadableStream__detach(JSC__JSValue possibleReadableStream, Zig::GlobalObject* globalObject) { - auto* readableStream = jsDynamicCast(JSC::JSValue::decode(possibleReadableStream)); + auto value = JSC::JSValue::decode(possibleReadableStream); + if (value.isEmpty() || !value.isCell()) + return; + + auto* readableStream = static_cast(value.asCell()); if (UNLIKELY(!readableStream)) return; - auto& vm = globalObject->vm(); - auto clientData = WebCore::clientData(vm); - readableStream->putDirect(vm, clientData->builtinNames().bunNativePtrPrivateName(), jsNumber(-1), 0); - readableStream->putDirect(vm, clientData->builtinNames().bunNativeTypePrivateName(), jsNumber(0), 0); - readableStream->putDirect(vm, clientData->builtinNames().disturbedPrivateName(), jsBoolean(true), 0); + readableStream->setNativePtr(globalObject->vm(), jsNumber(-1)); + readableStream->setNativeType(0); + readableStream->setDisturbed(true); } extern "C" bool ReadableStream__isDisturbed(JSC__JSValue possibleReadableStream, Zig::GlobalObject* globalObject); extern "C" bool ReadableStream__isDisturbed(JSC__JSValue possibleReadableStream, Zig::GlobalObject* globalObject) @@ -2464,12 +2450,12 @@ extern "C" bool ReadableStream__isLocked(JSC__JSValue possibleReadableStream, Zi return stream != nullptr && ReadableStream::isLocked(globalObject, stream); } -extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC__JSValue* possibleReadableStream, JSValue* ptr) +extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC__JSValue* possibleReadableStream, void** ptr) { ASSERT(globalObject); JSC::JSObject* object = JSValue::decode(*possibleReadableStream).getObject(); if (!object) { - *ptr = JSC::JSValue(); + *ptr = nullptr; return -1; } @@ -2491,12 +2477,12 @@ extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JS } if (UNLIKELY(throwScope.exception())) { - *ptr = JSC::JSValue(); + *ptr = nullptr; return -1; } if (fn.isEmpty()) { - *ptr = JSC::JSValue(); + *ptr = nullptr; return -1; } @@ -2513,7 +2499,7 @@ extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JS } if (!result.isObject()) { - *ptr = JSC::JSValue(); + *ptr = nullptr; return -1; } @@ -2521,26 +2507,37 @@ extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JS ASSERT(object->inherits()); *possibleReadableStream = JSValue::encode(object); - *ptr = JSValue(); + *ptr = nullptr; ensureStillAliveHere(object); return 0; } auto* readableStream = jsCast(object); - int32_t num = 0; - if (JSValue numberValue = readableStream->getDirect(vm, builtinNames.bunNativeTypePrivateName())) { - num = numberValue.toInt32(globalObject); + JSValue nativePtrHandle = readableStream->nativePtr(); + if (nativePtrHandle.isEmpty() || !nativePtrHandle.isCell()) { + *ptr = nullptr; + return 0; } - // If this type is outside the expected range, it means something is wrong. - if (UNLIKELY(!(num > 0 && num < 5))) { - *ptr = JSC::JSValue(); - return 0; + JSCell* cell = nativePtrHandle.asCell(); + + if (auto* casted = jsDynamicCast(cell)) { + *ptr = casted->wrapped(); + return 1; } - *ptr = readableStream->getDirect(vm, builtinNames.bunNativePtrPrivateName()); - return num; + if (auto* casted = jsDynamicCast(cell)) { + *ptr = casted->wrapped(); + return 2; + } + + if (auto* casted = jsDynamicCast(cell)) { + *ptr = casted->wrapped(); + return 4; + } + + return 0; } extern "C" JSC__JSValue ReadableStream__consume(Zig::GlobalObject* globalObject, JSC__JSValue stream, JSC__JSValue nativeType, JSC__JSValue nativePtr); @@ -2564,8 +2561,7 @@ extern "C" JSC__JSValue ReadableStream__consume(Zig::GlobalObject* globalObject, return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); } -extern "C" JSC__JSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC__JSValue nativeType, JSC__JSValue nativePtr); -extern "C" JSC__JSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC__JSValue nativeType, JSC__JSValue nativePtr) +extern "C" JSC__JSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC__JSValue nativePtr) { auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -2575,7 +2571,6 @@ extern "C" JSC__JSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalO auto function = globalObject->getDirect(vm, builtinNames.createNativeReadableStreamPrivateName()).getObject(); JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(nativeType)); arguments.append(JSValue::decode(nativePtr)); auto callData = JSC::getCallData(function); @@ -3314,12 +3309,6 @@ void GlobalObject::finishCreation(VM& vm) init.set(prototype); }); - m_JSUVStreamSinkControllerPrototype.initLater( - [](const JSC::LazyProperty::Initializer& init) { - auto* prototype = createJSSinkControllerPrototype(init.vm, init.owner, WebCore::SinkID::UVStreamSink); - init.set(prototype); - }); - m_performanceObject.initLater( [](const JSC::LazyProperty::Initializer& init) { auto* globalObject = reinterpret_cast(init.owner); @@ -3488,11 +3477,11 @@ void GlobalObject::finishCreation(VM& vm) init.setConstructor(constructor); }); - m_JSUVStreamSinkClassStructure.initLater( + m_JSFileSinkClassStructure.initLater( [](LazyClassStructure::Initializer& init) { - auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::UVStreamSink); - auto* structure = JSUVStreamSink::createStructure(init.vm, init.global, prototype); - auto* constructor = JSUVStreamSinkConstructor::create(init.vm, init.global, JSUVStreamSinkConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), jsCast(prototype)); + auto* prototype = createJSSinkPrototype(init.vm, init.global, WebCore::SinkID::FileSink); + auto* structure = JSFileSink::createStructure(init.vm, init.global, prototype); + auto* constructor = JSFileSinkConstructor::create(init.vm, init.global, JSFileSinkConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), jsCast(prototype)); init.setPrototype(prototype); init.setStructure(structure); init.setConstructor(constructor); @@ -3828,49 +3817,6 @@ JSC_DEFINE_CUSTOM_GETTER(functionLazyNavigatorGetter, return JSC::JSValue::encode(reinterpret_cast(globalObject)->navigatorObject()); } -JSC_DEFINE_HOST_FUNCTION(functionGetDirectStreamDetails, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) -{ - auto* globalObject = reinterpret_cast(lexicalGlobalObject); - JSC::VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - auto argCount = callFrame->argumentCount(); - if (argCount != 1) { - return JSC::JSValue::encode(JSC::jsNull()); - } - - auto stream = callFrame->argument(0); - if (!stream.isObject()) { - return JSC::JSValue::encode(JSC::jsNull()); - } - - auto* streamObject = stream.getObject(); - auto* readableStream = jsDynamicCast(streamObject); - if (!readableStream) { - return JSC::JSValue::encode(JSC::jsNull()); - } - - auto clientData = WebCore::clientData(vm); - - JSValue ptrValue = readableStream->get(globalObject, clientData->builtinNames().bunNativePtrPrivateName()); - JSValue typeValue = readableStream->get(globalObject, clientData->builtinNames().bunNativeTypePrivateName()); - auto result = ptrValue.asAnyInt(); - - if (result == 0 || !typeValue.isNumber()) { - return JSC::JSValue::encode(JSC::jsNull()); - } - - readableStream->putDirect(vm, clientData->builtinNames().bunNativePtrPrivateName(), jsNumber(0), 0); - // -1 === detached - readableStream->putDirect(vm, clientData->builtinNames().bunNativeTypePrivateName(), jsNumber(-1), 0); - readableStream->putDirect(vm, clientData->builtinNames().disturbedPrivateName(), jsBoolean(true), 0); - - auto* resultObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); - resultObject->putDirect(vm, clientData->builtinNames().streamPublicName(), ptrValue, 0); - resultObject->putDirect(vm, clientData->builtinNames().dataPublicName(), typeValue, 0); - - return JSC::JSValue::encode(resultObject); -} - JSC::GCClient::IsoSubspace* GlobalObject::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl( @@ -3936,7 +3882,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) GlobalPropertyInfo(builtinNames.getInternalWritableStreamPrivateName(), JSFunction::create(vm, this, 1, String(), getInternalWritableStream, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.createWritableStreamFromInternalPrivateName(), JSFunction::create(vm, this, 1, String(), createWritableStreamFromInternal, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.fulfillModuleSyncPrivateName(), JSFunction::create(vm, this, 1, String(), functionFulfillModuleSync, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), - GlobalPropertyInfo(builtinNames.directPrivateName(), JSFunction::create(vm, this, 1, String(), functionGetDirectStreamDetails, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(vm.propertyNames->builtinNames().ArrayBufferPrivateName(), arrayBufferConstructor(), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.LoaderPrivateName(), this->moduleLoader(), PropertyAttribute::DontDelete | 0), GlobalPropertyInfo(builtinNames.internalModuleRegistryPrivateName(), this->internalModuleRegistry(), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), @@ -4089,77 +4034,71 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_nextTickQueue); visitor.append(thisObject->m_errorConstructorPrepareStackTraceValue); + thisObject->m_asyncBoundFunctionStructure.visit(visitor); + thisObject->m_bunObject.visit(visitor); + thisObject->m_bunSleepThenCallback.visit(visitor); + thisObject->m_cachedGlobalObjectStructure.visit(visitor); + thisObject->m_cachedGlobalProxyStructure.visit(visitor); + thisObject->m_callSiteStructure.visit(visitor); + thisObject->m_commonJSModuleObjectStructure.visit(visitor); + thisObject->m_cryptoObject.visit(visitor); + thisObject->m_emitReadableNextTickFunction.visit(visitor); + thisObject->m_encodeIntoObjectStructure.visit(visitor); + thisObject->m_errorConstructorPrepareStackTraceInternalValue.visit(visitor); + thisObject->m_esmRegistryMap.visit(visitor); + thisObject->m_importMetaObjectStructure.visit(visitor); + thisObject->m_internalModuleRegistry.visit(visitor); + thisObject->m_JSArrayBufferControllerPrototype.visit(visitor); thisObject->m_JSArrayBufferSinkClassStructure.visit(visitor); + thisObject->m_JSBufferClassStructure.visit(visitor); thisObject->m_JSBufferListClassStructure.visit(visitor); + thisObject->m_JSBufferSubclassStructure.visit(visitor); + thisObject->m_JSCryptoKey.visit(visitor); + thisObject->m_JSDOMFileConstructor.visit(visitor); thisObject->m_JSFFIFunctionStructure.visit(visitor); thisObject->m_JSFileSinkClassStructure.visit(visitor); + thisObject->m_JSFileSinkControllerPrototype.visit(visitor); + thisObject->m_JSHTTPResponseController.visit(visitor); thisObject->m_JSHTTPResponseSinkClassStructure.visit(visitor); + thisObject->m_JSHTTPSResponseControllerPrototype.visit(visitor); thisObject->m_JSHTTPSResponseSinkClassStructure.visit(visitor); thisObject->m_JSReadableStateClassStructure.visit(visitor); + thisObject->m_JSSocketAddressStructure.visit(visitor); + thisObject->m_JSSQLStatementStructure.visit(visitor); thisObject->m_JSStringDecoderClassStructure.visit(visitor); + thisObject->m_lazyPreloadTestModuleObject.visit(visitor); + thisObject->m_lazyReadableStreamPrototypeMap.visit(visitor); + thisObject->m_lazyRequireCacheObject.visit(visitor); + thisObject->m_lazyTestModuleObject.visit(visitor); + thisObject->m_memoryFootprintStructure.visit(visitor); thisObject->m_NapiClassStructure.visit(visitor); - thisObject->m_JSBufferClassStructure.visit(visitor); + thisObject->m_NapiExternalStructure.visit(visitor); + thisObject->m_NAPIFunctionStructure.visit(visitor); + thisObject->m_NapiPrototypeStructure.visit(visitor); + thisObject->m_nativeMicrotaskTrampoline.visit(visitor); + thisObject->m_navigatorObject.visit(visitor); thisObject->m_NodeVMScriptClassStructure.visit(visitor); - thisObject->m_pendingVirtualModuleResultStructure.visit(visitor); + thisObject->m_performanceObject.visit(visitor); thisObject->m_performMicrotaskFunction.visit(visitor); thisObject->m_performMicrotaskVariadicFunction.visit(visitor); - thisObject->m_utilInspectFunction.visit(visitor); - thisObject->m_utilInspectStylizeColorFunction.visit(visitor); - thisObject->m_utilInspectStylizeNoColorFunction.visit(visitor); - thisObject->m_lazyReadableStreamPrototypeMap.visit(visitor); - thisObject->m_requireMap.visit(visitor); - thisObject->m_esmRegistryMap.visit(visitor); - thisObject->m_encodeIntoObjectStructure.visit(visitor); - thisObject->m_JSArrayBufferControllerPrototype.visit(visitor); - thisObject->m_JSFileSinkControllerPrototype.visit(visitor); - thisObject->m_JSHTTPSResponseControllerPrototype.visit(visitor); - thisObject->m_JSUVStreamSinkControllerPrototype.visit(visitor); - thisObject->m_navigatorObject.visit(visitor); - thisObject->m_nativeMicrotaskTrampoline.visit(visitor); - thisObject->m_performanceObject.visit(visitor); thisObject->m_processEnvObject.visit(visitor); thisObject->m_processObject.visit(visitor); - thisObject->m_bunObject.visit(visitor); - thisObject->m_subtleCryptoObject.visit(visitor); - thisObject->m_JSHTTPResponseController.visit(visitor); - thisObject->m_callSiteStructure.visit(visitor); - thisObject->m_emitReadableNextTickFunction.visit(visitor); - thisObject->m_JSBufferSubclassStructure.visit(visitor); - thisObject->m_JSCryptoKey.visit(visitor); - - thisObject->m_cryptoObject.visit(visitor); - thisObject->m_JSDOMFileConstructor.visit(visitor); - thisObject->m_requireFunctionUnbound.visit(visitor); + thisObject->m_requireMap.visit(visitor); thisObject->m_requireResolveFunctionUnbound.visit(visitor); - thisObject->m_importMetaObjectStructure.visit(visitor); - thisObject->m_asyncBoundFunctionStructure.visit(visitor); - thisObject->m_internalModuleRegistry.visit(visitor); - - thisObject->m_lazyRequireCacheObject.visit(visitor); - thisObject->m_vmModuleContextMap.visit(visitor); - thisObject->m_errorConstructorPrepareStackTraceInternalValue.visit(visitor); - thisObject->m_bunSleepThenCallback.visit(visitor); - thisObject->m_lazyTestModuleObject.visit(visitor); - thisObject->m_lazyPreloadTestModuleObject.visit(visitor); + thisObject->m_subtleCryptoObject.visit(visitor); thisObject->m_testMatcherUtilsObject.visit(visitor); - thisObject->m_commonJSModuleObjectStructure.visit(visitor); - thisObject->m_JSSQLStatementStructure.visit(visitor); - thisObject->m_memoryFootprintStructure.visit(visitor); - thisObject->m_JSSocketAddressStructure.visit(visitor); - thisObject->m_cachedGlobalObjectStructure.visit(visitor); - thisObject->m_cachedGlobalProxyStructure.visit(visitor); - thisObject->m_NapiExternalStructure.visit(visitor); - thisObject->m_NapiPrototypeStructure.visit(visitor); - thisObject->m_NAPIFunctionStructure.visit(visitor); - + thisObject->m_utilInspectFunction.visit(visitor); + thisObject->m_utilInspectStylizeColorFunction.visit(visitor); + thisObject->m_utilInspectStylizeNoColorFunction.visit(visitor); + thisObject->m_vmModuleContextMap.visit(visitor); + thisObject->mockModule.activeSpySetStructure.visit(visitor); thisObject->mockModule.mockFunctionStructure.visit(visitor); - thisObject->mockModule.mockResultStructure.visit(visitor); thisObject->mockModule.mockImplementationStructure.visit(visitor); - thisObject->mockModule.mockObjectStructure.visit(visitor); thisObject->mockModule.mockModuleStructure.visit(visitor); - thisObject->mockModule.activeSpySetStructure.visit(visitor); + thisObject->mockModule.mockObjectStructure.visit(visitor); + thisObject->mockModule.mockResultStructure.visit(visitor); thisObject->mockModule.mockWithImplementationCleanupDataStructure.visit(visitor); thisObject->mockModule.withImplementationCleanupFunction.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index dac97ca1224f0d..e8db205154566f 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -173,110 +173,105 @@ class GlobalObject : public JSC::JSGlobalObject { static void promiseRejectionTracker(JSGlobalObject*, JSC::JSPromise*, JSC::JSPromiseRejectionOperation); void setConsole(void* console); WebCore::JSBuiltinInternalFunctions& builtinInternalFunctions() { return m_builtinInternalFunctions; } - JSC::Structure* FFIFunctionStructure() { return m_JSFFIFunctionStructure.getInitializedOnMainThread(this); } - JSC::Structure* NapiClassStructure() { return m_NapiClassStructure.getInitializedOnMainThread(this); } + JSC::Structure* FFIFunctionStructure() const { return m_JSFFIFunctionStructure.getInitializedOnMainThread(this); } + JSC::Structure* NapiClassStructure() const { return m_NapiClassStructure.getInitializedOnMainThread(this); } - JSC::Structure* FileSinkStructure() { return m_JSFileSinkClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* FileSink() { return m_JSFileSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue FileSinkPrototype() { return m_JSFileSinkClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSValue JSReadableFileSinkControllerPrototype() { return m_JSFileSinkControllerPrototype.getInitializedOnMainThread(this); } + JSC::Structure* FileSinkStructure() const { return m_JSFileSinkClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* FileSink() const { return m_JSFileSinkClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue FileSinkPrototype() const { return m_JSFileSinkClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue JSReadableFileSinkControllerPrototype() const { return m_JSFileSinkControllerPrototype.getInitializedOnMainThread(this); } - JSC::Structure* JSBufferStructure() { return m_JSBufferClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* JSBufferConstructor() { return m_JSBufferClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue JSBufferPrototype() { return m_JSBufferClassStructure.prototypeInitializedOnMainThread(this); } - JSC::Structure* JSBufferSubclassStructure() { return m_JSBufferSubclassStructure.getInitializedOnMainThread(this); } + JSC::Structure* JSBufferStructure() const { return m_JSBufferClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* JSBufferConstructor() const { return m_JSBufferClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue JSBufferPrototype() const { return m_JSBufferClassStructure.prototypeInitializedOnMainThread(this); } + JSC::Structure* JSBufferSubclassStructure() const { return m_JSBufferSubclassStructure.getInitializedOnMainThread(this); } - JSC::Structure* JSCryptoKeyStructure() { return m_JSCryptoKey.getInitializedOnMainThread(this); } + JSC::Structure* JSCryptoKeyStructure() const { return m_JSCryptoKey.getInitializedOnMainThread(this); } - JSC::Structure* ArrayBufferSinkStructure() { return m_JSArrayBufferSinkClassStructure.getInitializedOnMainThread(this); } + JSC::Structure* ArrayBufferSinkStructure() const { return m_JSArrayBufferSinkClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* ArrayBufferSink() { return m_JSArrayBufferSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue ArrayBufferSinkPrototype() { return m_JSArrayBufferSinkClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSValue JSReadableArrayBufferSinkControllerPrototype() { return m_JSArrayBufferControllerPrototype.getInitializedOnMainThread(this); } + JSC::JSValue ArrayBufferSinkPrototype() const { return m_JSArrayBufferSinkClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue JSReadableArrayBufferSinkControllerPrototype() const { return m_JSArrayBufferControllerPrototype.getInitializedOnMainThread(this); } - JSC::Structure* HTTPResponseSinkStructure() { return m_JSHTTPResponseSinkClassStructure.getInitializedOnMainThread(this); } + JSC::Structure* HTTPResponseSinkStructure() const { return m_JSHTTPResponseSinkClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* HTTPResponseSink() { return m_JSHTTPResponseSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue HTTPResponseSinkPrototype() { return m_JSHTTPResponseSinkClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue HTTPResponseSinkPrototype() const { return m_JSHTTPResponseSinkClassStructure.prototypeInitializedOnMainThread(this); } JSC::Structure* JSReadableHTTPResponseSinkController() { return m_JSHTTPResponseController.getInitializedOnMainThread(this); } - JSC::Structure* HTTPSResponseSinkStructure() { return m_JSHTTPSResponseSinkClassStructure.getInitializedOnMainThread(this); } + JSC::Structure* HTTPSResponseSinkStructure() const { return m_JSHTTPSResponseSinkClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* HTTPSResponseSink() { return m_JSHTTPSResponseSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue HTTPSResponseSinkPrototype() { return m_JSHTTPSResponseSinkClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSValue JSReadableHTTPSResponseSinkControllerPrototype() { return m_JSHTTPSResponseControllerPrototype.getInitializedOnMainThread(this); } + JSC::JSValue HTTPSResponseSinkPrototype() const { return m_JSHTTPSResponseSinkClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue JSReadableHTTPSResponseSinkControllerPrototype() const { return m_JSHTTPSResponseControllerPrototype.getInitializedOnMainThread(this); } - JSC::Structure* UVStreamSinkStructure() { return m_JSUVStreamSinkClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* UVStreamSink() { return m_JSUVStreamSinkClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue UVStreamSinkPrototype() { return m_JSUVStreamSinkClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSValue JSReadableUVStreamSinkControllerPrototype() { return m_JSUVStreamSinkControllerPrototype.getInitializedOnMainThread(this); } - - JSC::Structure* JSBufferListStructure() { return m_JSBufferListClassStructure.getInitializedOnMainThread(this); } + JSC::Structure* JSBufferListStructure() const { return m_JSBufferListClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* JSBufferList() { return m_JSBufferListClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue JSBufferListPrototype() { return m_JSBufferListClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSValue JSBufferListPrototype() const { return m_JSBufferListClassStructure.prototypeInitializedOnMainThread(this); } - JSC::Structure* JSStringDecoderStructure() { return m_JSStringDecoderClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* JSStringDecoder() { return m_JSStringDecoderClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue JSStringDecoderPrototype() { return m_JSStringDecoderClassStructure.prototypeInitializedOnMainThread(this); } + JSC::Structure* JSStringDecoderStructure() const { return m_JSStringDecoderClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* JSStringDecoder() const { return m_JSStringDecoderClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue JSStringDecoderPrototype() const { return m_JSStringDecoderClassStructure.prototypeInitializedOnMainThread(this); } - JSC::Structure* JSReadableStateStructure() { return m_JSReadableStateClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* JSReadableState() { return m_JSReadableStateClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue JSReadableStatePrototype() { return m_JSReadableStateClassStructure.prototypeInitializedOnMainThread(this); } + JSC::Structure* JSReadableStateStructure() const { return m_JSReadableStateClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* JSReadableState() const { return m_JSReadableStateClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue JSReadableStatePrototype() const { return m_JSReadableStateClassStructure.prototypeInitializedOnMainThread(this); } - JSC::Structure* NodeVMScriptStructure() { return m_NodeVMScriptClassStructure.getInitializedOnMainThread(this); } - JSC::JSObject* NodeVMScript() { return m_NodeVMScriptClassStructure.constructorInitializedOnMainThread(this); } - JSC::JSValue NodeVMScriptPrototype() { return m_NodeVMScriptClassStructure.prototypeInitializedOnMainThread(this); } + JSC::Structure* NodeVMScriptStructure() const { return m_NodeVMScriptClassStructure.getInitializedOnMainThread(this); } + JSC::JSObject* NodeVMScript() const { return m_NodeVMScriptClassStructure.constructorInitializedOnMainThread(this); } + JSC::JSValue NodeVMScriptPrototype() const { return m_NodeVMScriptClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSMap* readableStreamNativeMap() { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); } - JSC::JSMap* requireMap() { return m_requireMap.getInitializedOnMainThread(this); } - JSC::JSMap* esmRegistryMap() { return m_esmRegistryMap.getInitializedOnMainThread(this); } - JSC::Structure* encodeIntoObjectStructure() { return m_encodeIntoObjectStructure.getInitializedOnMainThread(this); } + JSC::JSMap* readableStreamNativeMap() const { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); } + JSC::JSMap* requireMap() const { return m_requireMap.getInitializedOnMainThread(this); } + JSC::JSMap* esmRegistryMap() const { return m_esmRegistryMap.getInitializedOnMainThread(this); } + JSC::Structure* encodeIntoObjectStructure() const { return m_encodeIntoObjectStructure.getInitializedOnMainThread(this); } JSC::Structure* callSiteStructure() const { return m_callSiteStructure.getInitializedOnMainThread(this); } - JSC::JSObject* performanceObject() { return m_performanceObject.getInitializedOnMainThread(this); } + JSC::JSObject* performanceObject() const { return m_performanceObject.getInitializedOnMainThread(this); } - JSC::JSFunction* performMicrotaskFunction() { return m_performMicrotaskFunction.getInitializedOnMainThread(this); } - JSC::JSFunction* performMicrotaskVariadicFunction() { return m_performMicrotaskVariadicFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* performMicrotaskFunction() const { return m_performMicrotaskFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* performMicrotaskVariadicFunction() const { return m_performMicrotaskVariadicFunction.getInitializedOnMainThread(this); } - JSC::JSFunction* utilInspectFunction() { return m_utilInspectFunction.getInitializedOnMainThread(this); } - JSC::JSFunction* utilInspectStylizeColorFunction() { return m_utilInspectStylizeColorFunction.getInitializedOnMainThread(this); } - JSC::JSFunction* utilInspectStylizeNoColorFunction() { return m_utilInspectStylizeNoColorFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* utilInspectFunction() const { return m_utilInspectFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* utilInspectStylizeColorFunction() const { return m_utilInspectStylizeColorFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* utilInspectStylizeNoColorFunction() const { return m_utilInspectStylizeNoColorFunction.getInitializedOnMainThread(this); } - JSC::JSFunction* emitReadableNextTickFunction() { return m_emitReadableNextTickFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* emitReadableNextTickFunction() const { return m_emitReadableNextTickFunction.getInitializedOnMainThread(this); } - JSObject* requireFunctionUnbound() { return m_requireFunctionUnbound.getInitializedOnMainThread(this); } - JSObject* requireResolveFunctionUnbound() { return m_requireResolveFunctionUnbound.getInitializedOnMainThread(this); } - Bun::InternalModuleRegistry* internalModuleRegistry() { return m_internalModuleRegistry.getInitializedOnMainThread(this); } + JSObject* requireFunctionUnbound() const { return m_requireFunctionUnbound.getInitializedOnMainThread(this); } + JSObject* requireResolveFunctionUnbound() const { return m_requireResolveFunctionUnbound.getInitializedOnMainThread(this); } + Bun::InternalModuleRegistry* internalModuleRegistry() const { return m_internalModuleRegistry.getInitializedOnMainThread(this); } - JSObject* processBindingConstants() { return m_processBindingConstants.getInitializedOnMainThread(this); } + JSObject* processBindingConstants() const { return m_processBindingConstants.getInitializedOnMainThread(this); } - JSObject* lazyRequireCacheObject() { return m_lazyRequireCacheObject.getInitializedOnMainThread(this); } + JSObject* lazyRequireCacheObject() const { return m_lazyRequireCacheObject.getInitializedOnMainThread(this); } - JSFunction* bunSleepThenCallback() { return m_bunSleepThenCallback.getInitializedOnMainThread(this); } + JSFunction* bunSleepThenCallback() const { return m_bunSleepThenCallback.getInitializedOnMainThread(this); } - Structure* globalObjectStructure() { return m_cachedGlobalObjectStructure.getInitializedOnMainThread(this); } - Structure* globalProxyStructure() { return m_cachedGlobalProxyStructure.getInitializedOnMainThread(this); } - JSObject* lazyTestModuleObject() { return m_lazyTestModuleObject.getInitializedOnMainThread(this); } - JSObject* lazyPreloadTestModuleObject() { return m_lazyPreloadTestModuleObject.getInitializedOnMainThread(this); } - Structure* CommonJSModuleObjectStructure() { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); } - Structure* ImportMetaObjectStructure() { return m_importMetaObjectStructure.getInitializedOnMainThread(this); } - Structure* AsyncContextFrameStructure() { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); } + Structure* globalObjectStructure() const { return m_cachedGlobalObjectStructure.getInitializedOnMainThread(this); } + Structure* globalProxyStructure() const { return m_cachedGlobalProxyStructure.getInitializedOnMainThread(this); } + JSObject* lazyTestModuleObject() const { return m_lazyTestModuleObject.getInitializedOnMainThread(this); } + JSObject* lazyPreloadTestModuleObject() const { return m_lazyPreloadTestModuleObject.getInitializedOnMainThread(this); } + Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); } + Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); } + Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); } - Structure* JSSocketAddressStructure() { return m_JSSocketAddressStructure.getInitializedOnMainThread(this); } + Structure* JSSocketAddressStructure() const { return m_JSSocketAddressStructure.getInitializedOnMainThread(this); } - JSWeakMap* vmModuleContextMap() { return m_vmModuleContextMap.getInitializedOnMainThread(this); } + JSWeakMap* vmModuleContextMap() const { return m_vmModuleContextMap.getInitializedOnMainThread(this); } - Structure* NapiExternalStructure() { return m_NapiExternalStructure.getInitializedOnMainThread(this); } - Structure* NapiPrototypeStructure() { return m_NapiPrototypeStructure.getInitializedOnMainThread(this); } - Structure* NAPIFunctionStructure() { return m_NAPIFunctionStructure.getInitializedOnMainThread(this); } + Structure* NapiExternalStructure() const { return m_NapiExternalStructure.getInitializedOnMainThread(this); } + Structure* NapiPrototypeStructure() const { return m_NapiPrototypeStructure.getInitializedOnMainThread(this); } + Structure* NAPIFunctionStructure() const { return m_NAPIFunctionStructure.getInitializedOnMainThread(this); } - Structure* JSSQLStatementStructure() { return m_JSSQLStatementStructure.getInitializedOnMainThread(this); } + Structure* JSSQLStatementStructure() const { return m_JSSQLStatementStructure.getInitializedOnMainThread(this); } bool hasProcessObject() const { return m_processObject.isInitialized(); } RefPtr performance(); - JSC::JSObject* processObject() { return m_processObject.getInitializedOnMainThread(this); } - JSC::JSObject* processEnvObject() { return m_processEnvObject.getInitializedOnMainThread(this); } - JSC::JSObject* bunObject() { return m_bunObject.getInitializedOnMainThread(this); } + JSC::JSObject* processObject() const { return m_processObject.getInitializedOnMainThread(this); } + JSC::JSObject* processEnvObject() const { return m_processEnvObject.getInitializedOnMainThread(this); } + JSC::JSObject* bunObject() const { return m_bunObject.getInitializedOnMainThread(this); } void drainMicrotasks(); @@ -393,7 +388,7 @@ class GlobalObject : public JSC::JSGlobalObject { } JSObject* navigatorObject(); - JSFunction* nativeMicrotaskTrampoline() { return m_nativeMicrotaskTrampoline.getInitializedOnMainThread(this); } + JSFunction* nativeMicrotaskTrampoline() const { return m_nativeMicrotaskTrampoline.getInitializedOnMainThread(this); } String agentClusterID() const; static String defaultAgentClusterID(); @@ -445,8 +440,8 @@ class GlobalObject : public JSC::JSGlobalObject { LazyProperty m_processEnvObject; - JSObject* cryptoObject() { return m_cryptoObject.getInitializedOnMainThread(this); } - JSObject* JSDOMFileConstructor() { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); } + JSObject* cryptoObject() const { return m_cryptoObject.getInitializedOnMainThread(this); } + JSObject* JSDOMFileConstructor() const { return m_JSDOMFileConstructor.getInitializedOnMainThread(this); } Bun::CommonStrings& commonStrings() { return m_commonStrings; } #include "ZigGeneratedClasses+lazyStructureHeader.h" @@ -486,7 +481,6 @@ class GlobalObject : public JSC::JSGlobalObject { LazyClassStructure m_JSFileSinkClassStructure; LazyClassStructure m_JSHTTPResponseSinkClassStructure; LazyClassStructure m_JSHTTPSResponseSinkClassStructure; - LazyClassStructure m_JSUVStreamSinkClassStructure; LazyClassStructure m_JSReadableStateClassStructure; LazyClassStructure m_JSStringDecoderClassStructure; LazyClassStructure m_NapiClassStructure; @@ -517,9 +511,8 @@ class GlobalObject : public JSC::JSGlobalObject { LazyProperty m_esmRegistryMap; LazyProperty m_encodeIntoObjectStructure; LazyProperty m_JSArrayBufferControllerPrototype; - LazyProperty m_JSFileSinkControllerPrototype; LazyProperty m_JSHTTPSResponseControllerPrototype; - LazyProperty m_JSUVStreamSinkControllerPrototype; + LazyProperty m_JSFileSinkControllerPrototype; LazyProperty m_subtleCryptoObject; LazyProperty m_JSHTTPResponseController; LazyProperty m_JSBufferSubclassStructure; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 393df8ba00f100..f56b406ecb4d02 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4740,6 +4740,9 @@ enum class BuiltinNamesMap : uint8_t { toString, redirect, inspectCustom, + highWaterMark, + path, + stream, asyncIterator, }; @@ -4778,9 +4781,22 @@ static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigne case BuiltinNamesMap::inspectCustom: { return Identifier::fromUid(vm.symbolRegistry().symbolForKey("nodejs.util.inspect.custom"_s)); } + case BuiltinNamesMap::highWaterMark: { + return clientData->builtinNames().highWaterMarkPublicName(); + } + case BuiltinNamesMap::path: { + return clientData->builtinNames().pathPublicName(); + } + case BuiltinNamesMap::stream: { + return clientData->builtinNames().streamPublicName(); + } case BuiltinNamesMap::asyncIterator: { return vm.propertyNames->asyncIteratorSymbol; } + default: { + ASSERT_NOT_REACHED(); + return Identifier(); + } } } @@ -5178,12 +5194,33 @@ extern "C" void JSC__JSGlobalObject__queueMicrotaskJob(JSC__JSGlobalObject* arg0 { Zig::GlobalObject* globalObject = reinterpret_cast(arg0); JSC::VM& vm = globalObject->vm(); - globalObject->queueMicrotask( - JSValue(globalObject->performMicrotaskFunction()), - JSC::JSValue::decode(JSValue1), + JSValue microtaskArgs[] = { + JSValue::decode(JSValue1), globalObject->m_asyncContextData.get()->getInternalField(0), - JSC::JSValue::decode(JSValue3), - JSC::JSValue::decode(JSValue4)); + JSValue::decode(JSValue3), + JSValue::decode(JSValue4) + }; + + ASSERT(microtaskArgs[0].isCallable()); + + if (microtaskArgs[1].isEmpty()) { + microtaskArgs[1] = jsUndefined(); + } + + if (microtaskArgs[2].isEmpty()) { + microtaskArgs[2] = jsUndefined(); + } + + if (microtaskArgs[3].isEmpty()) { + microtaskArgs[3] = jsUndefined(); + } + + globalObject->queueMicrotask( + globalObject->performMicrotaskFunction(), + WTFMove(microtaskArgs[0]), + WTFMove(microtaskArgs[1]), + WTFMove(microtaskArgs[2]), + WTFMove(microtaskArgs[3])); } extern "C" WebCore::AbortSignal* WebCore__AbortSignal__new(JSC__JSGlobalObject* globalObject) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 5545c4f6c42a0a..96355e8ee14cd4 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1610,6 +1610,25 @@ pub const SystemError = extern struct { pub const name = "SystemError"; pub const namespace = ""; + pub fn getErrno(this: *const SystemError) bun.C.E { + // The inverse in bun.sys.Error.toSystemError() + return @enumFromInt(this.errno * -1); + } + + pub fn deref(this: *const SystemError) void { + this.path.deref(); + this.code.deref(); + this.message.deref(); + this.syscall.deref(); + } + + pub fn ref(this: *SystemError) void { + this.path.ref(); + this.code.ref(); + this.message.ref(); + this.syscall.ref(); + } + pub fn toErrorInstance(this: *const SystemError, global: *JSGlobalObject) JSValue { defer { this.path.deref(); @@ -2878,7 +2897,7 @@ pub const JSGlobalObject = extern struct { pub fn queueMicrotask( this: *JSGlobalObject, function: JSValue, - args: []JSC.JSValue, + args: []const JSC.JSValue, ) void { this.queueMicrotaskJob( function, @@ -3747,6 +3766,18 @@ pub const JSValue = enum(JSValueReprInt) { return FetchHeaders.cast(value); } + if (comptime ZigType == JSC.WebCore.Body.Value) { + if (value.as(JSC.WebCore.Request)) |req| { + return req.getBodyValue(); + } + + if (value.as(JSC.WebCore.Response)) |res| { + return res.getBodyValue(); + } + + return null; + } + if (comptime @hasDecl(ZigType, "fromJS") and @TypeOf(ZigType.fromJS) == fn (JSC.JSValue) ?*ZigType) { if (comptime ZigType == JSC.WebCore.Blob) { if (ZigType.fromJS(value)) |blob| { @@ -3905,7 +3936,7 @@ pub const JSValue = enum(JSValueReprInt) { .quote_strings = true, }; - JSC.ConsoleObject.format( + JSC.ConsoleObject.format2( .Debug, globalObject, @as([*]const JSValue, @ptrCast(&this)), @@ -4539,7 +4570,14 @@ pub const JSValue = enum(JSValueReprInt) { toString, redirect, inspectCustom, + highWaterMark, + path, + stream, asyncIterator, + + pub fn has(property: []const u8) bool { + return bun.ComptimeEnumMap(BuiltinName).has(property); + } }; // intended to be more lightweight than ZigString @@ -4610,6 +4648,12 @@ pub const JSValue = enum(JSValueReprInt) { } pub fn get(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { + if (comptime bun.Environment.isDebug) { + if (BuiltinName.has(property)) { + Output.debugWarn("get(\"{s}\") called. Please use fastGet(.{s}) instead!", .{ property, property }); + } + } + const value = getIfPropertyExistsImpl(this, global, property.ptr, @as(u32, @intCast(property.len))); return if (@intFromEnum(value) != 0) value else return null; } @@ -4637,6 +4681,19 @@ pub const JSValue = enum(JSValueReprInt) { return function.isCell() and function.isCallable(global.vm()); } + pub fn getTruthyComptime(this: JSValue, global: *JSGlobalObject, comptime property: []const u8) ?JSValue { + if (comptime bun.ComptimeEnumMap(BuiltinName).has(property)) { + if (fastGet(this, global, @field(BuiltinName, property))) |prop| { + if (prop.isEmptyOrUndefinedOrNull()) return null; + return prop; + } + + return null; + } + + return getTruthy(this, global, property); + } + pub fn getTruthy(this: JSValue, global: *JSGlobalObject, property: []const u8) ?JSValue { if (get(this, global, property)) |prop| { if (prop.isEmptyOrUndefinedOrNull()) return null; @@ -4694,6 +4751,15 @@ pub const JSValue = enum(JSValueReprInt) { } pub fn getOptionalEnum(this: JSValue, globalThis: *JSGlobalObject, comptime property_name: []const u8, comptime Enum: type) !?Enum { + if (comptime BuiltinName.has(property_name)) { + if (fastGet(this, globalThis, @field(BuiltinName, property_name))) |prop| { + if (prop.isEmptyOrUndefinedOrNull()) + return null; + return try toEnum(prop, globalThis, property_name, Enum); + } + return null; + } + if (get(this, globalThis, property_name)) |prop| { if (prop.isEmptyOrUndefinedOrNull()) return null; @@ -4746,7 +4812,12 @@ pub const JSValue = enum(JSValueReprInt) { } pub fn getOptional(this: JSValue, globalThis: *JSGlobalObject, comptime property_name: []const u8, comptime T: type) !?T { - if (getTruthy(this, globalThis, property_name)) |prop| { + const prop = (if (comptime BuiltinName.has(property_name)) + fastGet(this, globalThis, @field(BuiltinName, property_name)) + else + get(this, globalThis, property_name)) orelse return null; + + if (!prop.isEmptyOrUndefinedOrNull()) { switch (comptime T) { bool => { if (prop.isBoolean()) { diff --git a/src/bun.js/bindings/bun-spawn.cpp b/src/bun.js/bindings/bun-spawn.cpp index ff83c5a57a82a1..186ec4ad5ef9ba 100644 --- a/src/bun.js/bindings/bun-spawn.cpp +++ b/src/bun.js/bindings/bun-spawn.cpp @@ -48,6 +48,7 @@ typedef struct bun_spawn_request_t { extern "C" ssize_t posix_spawn_bun( int* pid, + const char* path, const bun_spawn_request_t* request, char* const argv[], char* const envp[]) @@ -58,7 +59,6 @@ extern "C" ssize_t posix_spawn_bun( sigfillset(&blockall); sigprocmask(SIG_SETMASK, &blockall, &oldmask); pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs); - const char* path = argv[0]; pid_t child = vfork(); const auto parentFailed = [&]() -> ssize_t { @@ -155,7 +155,9 @@ extern "C" ssize_t posix_spawn_bun( if (bun_close_range(current_max_fd + 1, ~0U, CLOSE_RANGE_CLOEXEC) != 0) { bun_close_range(current_max_fd + 1, ~0U, 0); } - execve(path, argv, envp); + if (execve(path, argv, envp) == -1) { + return childFailed(); + } _exit(127); // should never be reached. diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index c16f67a34c96b4..282682de95d590 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -121,6 +121,12 @@ extern "C" void dump_zone_malloc_stats() } } +#elif OS(DARWIN) + +extern "C" void dump_zone_malloc_stats() +{ +} + #endif #if OS(WINDOWS) @@ -269,4 +275,48 @@ void lshpack_wrapper_deinit(lshpack_wrapper* self) lshpack_enc_cleanup(&self->enc); self->free(self); } -} \ No newline at end of file +} + +#if OS(LINUX) + +#include + +static inline void make_pos_h_l(unsigned long* pos_h, unsigned long* pos_l, + off_t offset) +{ +#if __BITS_PER_LONG == 64 + *pos_l = offset; + *pos_h = 0; +#else + *pos_l = offset & 0xffffffff; + *pos_h = ((uint64_t)offset) >> 32; +#endif +} +extern "C" ssize_t sys_preadv2(int fd, const struct iovec* iov, int iovcnt, + off_t offset, unsigned int flags) +{ + return syscall(SYS_preadv2, fd, iov, iovcnt, offset, offset>>32, RWF_NOWAIT); +} +extern "C" ssize_t sys_pwritev2(int fd, const struct iovec* iov, int iovcnt, + off_t offset, unsigned int flags) +{ + unsigned long pos_l, pos_h; + + make_pos_h_l(&pos_h, &pos_l, offset); + return syscall(__NR_pwritev2, fd, iov, iovcnt, pos_l, pos_h, flags); +} +#else +extern "C" ssize_t preadv2(int fd, const struct iovec* iov, int iovcnt, + off_t offset, unsigned int flags) +{ + errno = ENOSYS; + return -1; +} +extern "C" ssize_t pwritev2(int fd, const struct iovec* iov, int iovcnt, + off_t offset, unsigned int flags) +{ + errno = ENOSYS; + return -1; +} + +#endif diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 83c0e68d00ce01..4b5d4fedc20753 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -132,17 +132,11 @@ pub const ZigErrorType = extern struct { pub const NodePath = JSC.Node.Path; -// Web Streams -pub const JSReadableStreamBlob = JSC.WebCore.ByteBlobLoader.Source.JSReadableStreamSource; -pub const JSReadableStreamFile = JSC.WebCore.FileReader.Source.JSReadableStreamSource; -pub const JSReadableStreamBytes = JSC.WebCore.ByteStream.Source.JSReadableStreamSource; - // Sinks pub const JSArrayBufferSink = JSC.WebCore.ArrayBufferSink.JSSink; pub const JSHTTPSResponseSink = JSC.WebCore.HTTPSResponseSink.JSSink; pub const JSHTTPResponseSink = JSC.WebCore.HTTPResponseSink.JSSink; pub const JSFileSink = JSC.WebCore.FileSink.JSSink; -pub const JSUVStreamSink = JSC.WebCore.UVStreamSink.JSSink; // WebSocket pub const WebSocketHTTPClient = @import("../../http/websocket_http_client.zig").WebSocketHTTPClient; @@ -753,6 +747,10 @@ pub const ZigException = extern struct { this.name.deref(); this.message.deref(); + for (this.stack.source_lines_ptr[0..this.stack.source_lines_len]) |*line| { + line.deref(); + } + for (this.stack.frames_ptr[0..this.stack.frames_len]) |*frame| { frame.deinit(); } @@ -770,6 +768,7 @@ pub const ZigException = extern struct { frames: [frame_count]ZigStackFrame, loaded: bool, zig_exception: ZigException, + need_to_clear_parser_arena_on_deinit: bool = false, pub const Zero: Holder = Holder{ .frames = brk: { @@ -796,8 +795,11 @@ pub const ZigException = extern struct { return Holder.Zero; } - pub fn deinit(this: *Holder) void { + pub fn deinit(this: *Holder, vm: *JSC.VirtualMachine) void { this.zigException().deinit(); + if (this.need_to_clear_parser_arena_on_deinit) { + vm.module_loader.resetArena(vm); + } } pub fn zigException(this: *Holder) *ZigException { @@ -911,14 +913,11 @@ comptime { _ = Process.setTitle; Bun.Timer.shim.ref(); NodePath.shim.ref(); - JSReadableStreamBlob.shim.ref(); JSArrayBufferSink.shim.ref(); JSHTTPResponseSink.shim.ref(); JSHTTPSResponseSink.shim.ref(); JSFileSink.shim.ref(); - JSUVStreamSink.shim.ref(); - JSReadableStreamBytes.shim.ref(); - JSReadableStreamFile.shim.ref(); + JSFileSink.shim.ref(); _ = ZigString__free; _ = ZigString__free_global; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index c56276c6dc32eb..5bc4d2aefbb4f3 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -65,4 +65,7 @@ pub const Classes = struct { pub const Crypto = JSC.WebCore.Crypto; pub const FFI = JSC.FFI; pub const H2FrameParser = JSC.API.H2FrameParser; + pub const FileInternalReadableStreamSource = JSC.WebCore.FileReader.Source; + pub const BlobInternalReadableStreamSource = JSC.WebCore.ByteBlobLoader.Source; + pub const BytesInternalReadableStreamSource = JSC.WebCore.ByteStream.Source; }; diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h index 7a95034c7b4340..8b5f146b518305 100644 --- a/src/bun.js/bindings/headers-cpp.h +++ b/src/bun.js/bindings/headers-cpp.h @@ -190,8 +190,8 @@ extern "C" const size_t Bun__Timer_object_align_ = alignof(Bun__Timer); extern "C" const size_t Bun__BodyValueBufferer_object_size_ = sizeof(Bun__BodyValueBufferer); extern "C" const size_t Bun__BodyValueBufferer_object_align_ = alignof(Bun__BodyValueBufferer); -const size_t sizes[39] = {sizeof(JSC::JSObject), sizeof(WebCore::DOMURL), sizeof(WebCore::DOMFormData), sizeof(WebCore::FetchHeaders), sizeof(SystemError), sizeof(JSC::JSCell), sizeof(JSC::JSString), sizeof(JSC::JSModuleLoader), sizeof(WebCore::AbortSignal), sizeof(JSC::JSPromise), sizeof(JSC::JSInternalPromise), sizeof(JSC::JSFunction), sizeof(JSC::JSGlobalObject), sizeof(JSC::JSMap), sizeof(JSC::JSValue), sizeof(JSC::Exception), sizeof(JSC::VM), sizeof(JSC::ThrowScope), sizeof(JSC::CatchScope), sizeof(FFI__ptr), sizeof(Reader__u8), sizeof(Reader__u16), sizeof(Reader__u32), sizeof(Reader__ptr), sizeof(Reader__i8), sizeof(Reader__i16), sizeof(Reader__i32), sizeof(Reader__f32), sizeof(Reader__f64), sizeof(Reader__i64), sizeof(Reader__u64), sizeof(Reader__intptr), sizeof(Zig::GlobalObject), sizeof(Bun__Path), sizeof(ArrayBufferSink), sizeof(HTTPSResponseSink), sizeof(HTTPResponseSink), sizeof(FileSink), sizeof(UVStreamSink)}; +const size_t sizes[39] = {sizeof(JSC::JSObject), sizeof(WebCore::DOMURL), sizeof(WebCore::DOMFormData), sizeof(WebCore::FetchHeaders), sizeof(SystemError), sizeof(JSC::JSCell), sizeof(JSC::JSString), sizeof(JSC::JSModuleLoader), sizeof(WebCore::AbortSignal), sizeof(JSC::JSPromise), sizeof(JSC::JSInternalPromise), sizeof(JSC::JSFunction), sizeof(JSC::JSGlobalObject), sizeof(JSC::JSMap), sizeof(JSC::JSValue), sizeof(JSC::Exception), sizeof(JSC::VM), sizeof(JSC::ThrowScope), sizeof(JSC::CatchScope), sizeof(FFI__ptr), sizeof(Reader__u8), sizeof(Reader__u16), sizeof(Reader__u32), sizeof(Reader__ptr), sizeof(Reader__i8), sizeof(Reader__i16), sizeof(Reader__i32), sizeof(Reader__f32), sizeof(Reader__f64), sizeof(Reader__i64), sizeof(Reader__u64), sizeof(Reader__intptr), sizeof(Zig::GlobalObject), sizeof(Bun__Path), sizeof(ArrayBufferSink), sizeof(HTTPSResponseSink), sizeof(HTTPResponseSink), sizeof(FileSink), sizeof(FileSink)}; -const char* names[39] = {"JSC__JSObject", "WebCore__DOMURL", "WebCore__DOMFormData", "WebCore__FetchHeaders", "SystemError", "JSC__JSCell", "JSC__JSString", "JSC__JSModuleLoader", "WebCore__AbortSignal", "JSC__JSPromise", "JSC__JSInternalPromise", "JSC__JSFunction", "JSC__JSGlobalObject", "JSC__JSMap", "JSC__JSValue", "JSC__Exception", "JSC__VM", "JSC__ThrowScope", "JSC__CatchScope", "FFI__ptr", "Reader__u8", "Reader__u16", "Reader__u32", "Reader__ptr", "Reader__i8", "Reader__i16", "Reader__i32", "Reader__f32", "Reader__f64", "Reader__i64", "Reader__u64", "Reader__intptr", "Zig__GlobalObject", "Bun__Path", "ArrayBufferSink", "HTTPSResponseSink", "HTTPResponseSink", "FileSink", "UVStreamSink"}; +const char* names[39] = {"JSC__JSObject", "WebCore__DOMURL", "WebCore__DOMFormData", "WebCore__FetchHeaders", "SystemError", "JSC__JSCell", "JSC__JSString", "JSC__JSModuleLoader", "WebCore__AbortSignal", "JSC__JSPromise", "JSC__JSInternalPromise", "JSC__JSFunction", "JSC__JSGlobalObject", "JSC__JSMap", "JSC__JSValue", "JSC__Exception", "JSC__VM", "JSC__ThrowScope", "JSC__CatchScope", "FFI__ptr", "Reader__u8", "Reader__u16", "Reader__u32", "Reader__ptr", "Reader__i8", "Reader__i16", "Reader__i32", "Reader__f32", "Reader__f64", "Reader__i64", "Reader__u64", "Reader__intptr", "Zig__GlobalObject", "Bun__Path", "ArrayBufferSink", "HTTPSResponseSink", "HTTPResponseSink", "FileSink", "FileSink"}; -const size_t aligns[39] = {alignof(JSC::JSObject), alignof(WebCore::DOMURL), alignof(WebCore::DOMFormData), alignof(WebCore::FetchHeaders), alignof(SystemError), alignof(JSC::JSCell), alignof(JSC::JSString), alignof(JSC::JSModuleLoader), alignof(WebCore::AbortSignal), alignof(JSC::JSPromise), alignof(JSC::JSInternalPromise), alignof(JSC::JSFunction), alignof(JSC::JSGlobalObject), alignof(JSC::JSMap), alignof(JSC::JSValue), alignof(JSC::Exception), alignof(JSC::VM), alignof(JSC::ThrowScope), alignof(JSC::CatchScope), alignof(FFI__ptr), alignof(Reader__u8), alignof(Reader__u16), alignof(Reader__u32), alignof(Reader__ptr), alignof(Reader__i8), alignof(Reader__i16), alignof(Reader__i32), alignof(Reader__f32), alignof(Reader__f64), alignof(Reader__i64), alignof(Reader__u64), alignof(Reader__intptr), alignof(Zig::GlobalObject), alignof(Bun__Path), alignof(ArrayBufferSink), alignof(HTTPSResponseSink), alignof(HTTPResponseSink), alignof(FileSink), alignof(UVStreamSink)}; +const size_t aligns[39] = {alignof(JSC::JSObject), alignof(WebCore::DOMURL), alignof(WebCore::DOMFormData), alignof(WebCore::FetchHeaders), alignof(SystemError), alignof(JSC::JSCell), alignof(JSC::JSString), alignof(JSC::JSModuleLoader), alignof(WebCore::AbortSignal), alignof(JSC::JSPromise), alignof(JSC::JSInternalPromise), alignof(JSC::JSFunction), alignof(JSC::JSGlobalObject), alignof(JSC::JSMap), alignof(JSC::JSValue), alignof(JSC::Exception), alignof(JSC::VM), alignof(JSC::ThrowScope), alignof(JSC::CatchScope), alignof(FFI__ptr), alignof(Reader__u8), alignof(Reader__u16), alignof(Reader__u32), alignof(Reader__ptr), alignof(Reader__i8), alignof(Reader__i16), alignof(Reader__i32), alignof(Reader__f32), alignof(Reader__f64), alignof(Reader__i64), alignof(Reader__u64), alignof(Reader__intptr), alignof(Zig::GlobalObject), alignof(Bun__Path), alignof(ArrayBufferSink), alignof(HTTPSResponseSink), alignof(HTTPResponseSink), alignof(FileSink), alignof(FileSink)}; diff --git a/src/bun.js/bindings/headers-replacements.zig b/src/bun.js/bindings/headers-replacements.zig index fb0834f1611213..39730a52f1c151 100644 --- a/src/bun.js/bindings/headers-replacements.zig +++ b/src/bun.js/bindings/headers-replacements.zig @@ -64,7 +64,6 @@ pub const struct_WebCore__FetchHeaders = bindings.FetchHeaders; pub const StringPointer = @import("../../api/schema.zig").Api.StringPointer; pub const struct_VirtualMachine = bindings.VirtualMachine; pub const ArrayBufferSink = @import("../webcore/streams.zig").ArrayBufferSink; -pub const UVStreamSink = @import("../webcore/streams.zig").UVStreamSink; pub const WebSocketHTTPClient = bindings.WebSocketHTTPClient; pub const WebSocketHTTPSClient = bindings.WebSocketHTTPSClient; pub const WebSocketClient = bindings.WebSocketClient; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index baa5e74641a4b0..4d448b015ab1d6 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -620,7 +620,7 @@ ZIG_DECL JSC__JSValue ByteStream__JSReadableStreamSource__load(JSC__JSGlobalObje #endif CPP_DECL JSC__JSValue ArrayBufferSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue ArrayBufferSink__createObject(JSC__JSGlobalObject* arg0, void* arg1); +CPP_DECL JSC__JSValue ArrayBufferSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); CPP_DECL void ArrayBufferSink__detachPtr(JSC__JSValue JSValue0); CPP_DECL void* ArrayBufferSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void ArrayBufferSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); @@ -640,7 +640,7 @@ ZIG_DECL JSC__JSValue ArrayBufferSink__write(JSC__JSGlobalObject* arg0, JSC__Cal #endif CPP_DECL JSC__JSValue HTTPSResponseSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue HTTPSResponseSink__createObject(JSC__JSGlobalObject* arg0, void* arg1); +CPP_DECL JSC__JSValue HTTPSResponseSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); CPP_DECL void HTTPSResponseSink__detachPtr(JSC__JSValue JSValue0); CPP_DECL void* HTTPSResponseSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void HTTPSResponseSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); @@ -660,7 +660,7 @@ ZIG_DECL JSC__JSValue HTTPSResponseSink__write(JSC__JSGlobalObject* arg0, JSC__C #endif CPP_DECL JSC__JSValue HTTPResponseSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue HTTPResponseSink__createObject(JSC__JSGlobalObject* arg0, void* arg1); +CPP_DECL JSC__JSValue HTTPResponseSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); CPP_DECL void HTTPResponseSink__detachPtr(JSC__JSValue JSValue0); CPP_DECL void* HTTPResponseSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void HTTPResponseSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); @@ -680,7 +680,7 @@ ZIG_DECL JSC__JSValue HTTPResponseSink__write(JSC__JSGlobalObject* arg0, JSC__Ca #endif CPP_DECL JSC__JSValue FileSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue FileSink__createObject(JSC__JSGlobalObject* arg0, void* arg1); +CPP_DECL JSC__JSValue FileSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); CPP_DECL void FileSink__detachPtr(JSC__JSValue JSValue0); CPP_DECL void* FileSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); CPP_DECL void FileSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); @@ -700,24 +700,24 @@ ZIG_DECL JSC__JSValue FileSink__write(JSC__JSGlobalObject* arg0, JSC__CallFrame* #endif -CPP_DECL JSC__JSValue UVStreamSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); -CPP_DECL JSC__JSValue UVStreamSink__createObject(JSC__JSGlobalObject* arg0, void* arg1); -CPP_DECL void UVStreamSink__detachPtr(JSC__JSValue JSValue0); -CPP_DECL void* UVStreamSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); -CPP_DECL void UVStreamSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); -CPP_DECL void UVStreamSink__onReady(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSValue JSValue2); +CPP_DECL JSC__JSValue FileSink__assignToStream(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, void* arg2, void** arg3); +CPP_DECL JSC__JSValue FileSink__createObject(JSC__JSGlobalObject* arg0, void* arg1, uintptr_t destructor); +CPP_DECL void FileSink__detachPtr(JSC__JSValue JSValue0); +CPP_DECL void* FileSink__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1); +CPP_DECL void FileSink__onClose(JSC__JSValue JSValue0, JSC__JSValue JSValue1); +CPP_DECL void FileSink__onReady(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSValue JSValue2); #ifdef __cplusplus -ZIG_DECL JSC__JSValue UVStreamSink__close(JSC__JSGlobalObject* arg0, void* arg1); -ZIG_DECL JSC__JSValue UVStreamSink__construct(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); -ZIG_DECL JSC__JSValue UVStreamSink__end(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); -ZIG_DECL JSC__JSValue UVStreamSink__endWithSink(void* arg0, JSC__JSGlobalObject* arg1); -ZIG_DECL void UVStreamSink__finalize(void* arg0); -ZIG_DECL JSC__JSValue UVStreamSink__flush(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); -ZIG_DECL JSC__JSValue UVStreamSink__start(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); -ZIG_DECL void UVStreamSink__updateRef(void* arg0, bool arg1); -ZIG_DECL JSC__JSValue UVStreamSink__write(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); +ZIG_DECL JSC__JSValue FileSink__close(JSC__JSGlobalObject* arg0, void* arg1); +ZIG_DECL JSC__JSValue FileSink__construct(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); +ZIG_DECL JSC__JSValue FileSink__end(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); +ZIG_DECL JSC__JSValue FileSink__endWithSink(void* arg0, JSC__JSGlobalObject* arg1); +ZIG_DECL void FileSink__finalize(void* arg0); +ZIG_DECL JSC__JSValue FileSink__flush(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); +ZIG_DECL JSC__JSValue FileSink__start(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); +ZIG_DECL void FileSink__updateRef(void* arg0, bool arg1); +ZIG_DECL JSC__JSValue FileSink__write(JSC__JSGlobalObject* arg0, JSC__CallFrame* arg1); #endif diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index db2807ce2fe885..4c202bb28d8aaf 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -363,35 +363,33 @@ pub extern fn Zig__GlobalObject__getModuleRegistryMap(arg0: *bindings.JSGlobalOb pub extern fn Zig__GlobalObject__resetModuleRegistryMap(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) bool; pub extern fn Bun__Path__create(arg0: *bindings.JSGlobalObject, arg1: bool) JSC__JSValue; pub extern fn ArrayBufferSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn ArrayBufferSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) JSC__JSValue; +pub extern fn ArrayBufferSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; pub extern fn ArrayBufferSink__detachPtr(JSValue0: JSC__JSValue) void; +pub extern fn ArrayBufferSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; pub extern fn ArrayBufferSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; pub extern fn ArrayBufferSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn ArrayBufferSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn HTTPSResponseSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn HTTPSResponseSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) JSC__JSValue; +pub extern fn HTTPSResponseSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; pub extern fn HTTPSResponseSink__detachPtr(JSValue0: JSC__JSValue) void; +pub extern fn HTTPSResponseSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; pub extern fn HTTPSResponseSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; pub extern fn HTTPSResponseSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn HTTPSResponseSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn HTTPResponseSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn HTTPResponseSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) JSC__JSValue; +pub extern fn HTTPResponseSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; pub extern fn HTTPResponseSink__detachPtr(JSValue0: JSC__JSValue) void; +pub extern fn HTTPResponseSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; pub extern fn HTTPResponseSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; pub extern fn HTTPResponseSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn HTTPResponseSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn FileSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn FileSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) JSC__JSValue; +pub extern fn FileSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque, onDestroyPtrTag: usize) JSC__JSValue; pub extern fn FileSink__detachPtr(JSValue0: JSC__JSValue) void; +pub extern fn FileSink__setDestroyCallback(JSValue0: JSC__JSValue, callback: usize) void; pub extern fn FileSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; pub extern fn FileSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; pub extern fn FileSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; -pub extern fn UVStreamSink__assignToStream(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, arg2: ?*anyopaque, arg3: [*c]*anyopaque) JSC__JSValue; -pub extern fn UVStreamSink__createObject(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) JSC__JSValue; -pub extern fn UVStreamSink__detachPtr(JSValue0: JSC__JSValue) void; -pub extern fn UVStreamSink__fromJS(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) ?*anyopaque; -pub extern fn UVStreamSink__onClose(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue) void; -pub extern fn UVStreamSink__onReady(JSValue0: JSC__JSValue, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue) void; pub extern fn ZigException__fromException(arg0: [*c]bindings.Exception) ZigException; pub const JSC__GetterSetter = bindings.GetterSetter; diff --git a/src/bun.js/bindings/webcore/JSReadableStream.cpp b/src/bun.js/bindings/webcore/JSReadableStream.cpp index 158e4421d77e54..e7e87202a41534 100644 --- a/src/bun.js/bindings/webcore/JSReadableStream.cpp +++ b/src/bun.js/bindings/webcore/JSReadableStream.cpp @@ -38,10 +38,13 @@ #include #include #include +#include "ZigGeneratedClasses.h" namespace WebCore { using namespace JSC; +extern "C" void ReadableStream__incrementCount(void*, int32_t); + // Functions // Attributes @@ -114,16 +117,59 @@ static const HashTableValue JSReadableStreamPrototypeTableValues[] = { { "pipeTo"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamPipeToCodeGenerator, 1 } }, { "pipeThrough"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamPipeThroughCodeGenerator, 2 } }, { "tee"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamTeeCodeGenerator, 0 } }, + }; const ClassInfo JSReadableStreamPrototype::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamPrototype) }; +static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__nativePtrSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + thisObject->setNativePtr(lexicalGlobalObject->vm(), JSValue::decode(encodedJSValue)); + return true; +} + +static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__nativePtrGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + return JSValue::encode(thisObject->nativePtr()); +} + +static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__nativeTypeSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + thisObject->setNativeType(JSValue::decode(encodedJSValue).toInt32(lexicalGlobalObject)); + return true; +} + +static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__nativeTypeGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + return JSValue::encode(jsNumber(thisObject->nativeType())); +} + +static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__disturbedSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + thisObject->setDisturbed(JSValue::decode(encodedJSValue).toBoolean(lexicalGlobalObject)); + return true; +} + +static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__disturbedGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) +{ + JSReadableStream* thisObject = jsCast(JSValue::decode(encodedThisValue)); + return JSValue::encode(jsBoolean(thisObject->disturbed())); +} + void JSReadableStreamPrototype::finishCreation(VM& vm) { Base::finishCreation(vm); auto clientData = WebCore::clientData(vm); - this->putDirect(vm, clientData->builtinNames().bunNativePtrPrivateName(), jsNumber(0), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - this->putDirect(vm, clientData->builtinNames().bunNativeTypePrivateName(), jsNumber(0), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); + + this->putDirectCustomAccessor(vm, clientData->builtinNames().bunNativePtrPrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__nativePtrGetterWrap, JSReadableStreamPrototype__nativePtrSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); + this->putDirectCustomAccessor(vm, clientData->builtinNames().bunNativeTypePrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__nativeTypeGetterWrap, JSReadableStreamPrototype__nativeTypeSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); + this->putDirectCustomAccessor(vm, clientData->builtinNames().disturbedPrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__disturbedGetterWrap, JSReadableStreamPrototype__disturbedSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); + reifyStaticProperties(vm, JSReadableStream::info(), JSReadableStreamPrototypeTableValues, *this); this->putDirectBuiltinFunction(vm, globalObject(), vm.propertyNames->asyncIteratorSymbol, readableStreamLazyAsyncIteratorCodeGenerator(vm), JSC::PropertyAttribute::DontDelete | 0); this->putDirectBuiltinFunction(vm, globalObject(), JSC::Identifier::fromString(vm, "values"_s), readableStreamValuesCodeGenerator(vm), JSC::PropertyAttribute::DontDelete | 0); @@ -143,6 +189,12 @@ void JSReadableStream::finishCreation(VM& vm) ASSERT(inherits(info())); } +void JSReadableStream::setNativePtr(JSC::VM& vm, JSC::JSValue value) +{ + + this->m_nativePtr.set(vm, this, value); +} + JSObject* JSReadableStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) { auto* structure = JSReadableStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); @@ -186,4 +238,16 @@ JSC::GCClient::IsoSubspace* JSReadableStream::subspaceForImpl(JSC::VM& vm) [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStream = std::forward(space); }); } +template +void JSReadableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSReadableStream* stream = jsCast(cell); + ASSERT_GC_OBJECT_INHERITS(stream, info()); + Base::visitChildren(stream, visitor); + + visitor.append(stream->m_nativePtr); +} + +DEFINE_VISIT_CHILDREN(JSReadableStream); + } diff --git a/src/bun.js/bindings/webcore/JSReadableStream.h b/src/bun.js/bindings/webcore/JSReadableStream.h index 137efe15d61690..c1022315c56b8d 100644 --- a/src/bun.js/bindings/webcore/JSReadableStream.h +++ b/src/bun.js/bindings/webcore/JSReadableStream.h @@ -25,6 +25,7 @@ namespace WebCore { class JSReadableStream : public JSDOMObject { + public: using Base = JSDOMObject; static JSReadableStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) @@ -54,7 +55,32 @@ class JSReadableStream : public JSDOMObject { } static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + int nativeType() const { return this->m_nativeType; } + bool disturbed() const { return this->m_disturbed; } + JSC::JSValue nativePtr() + { + return this->m_nativePtr.get(); + } + + void setNativePtr(JSC::VM&, JSC::JSValue value); + + void setNativeType(int value) + { + this->m_nativeType = value; + } + + void setDisturbed(bool value) + { + this->m_disturbed = value; + } + + DECLARE_VISIT_CHILDREN; + protected: + mutable JSC::WriteBarrier m_nativePtr; + int m_nativeType { 0 }; + bool m_disturbed = false; + JSReadableStream(JSC::Structure*, JSDOMGlobalObject&); void finishCreation(JSC::VM&); diff --git a/src/bun.js/bindings/webcore/JSReadableStreamSource.h b/src/bun.js/bindings/webcore/JSReadableStreamSource.h index 4a7fec950f8a67..49262b698695f9 100644 --- a/src/bun.js/bindings/webcore/JSReadableStreamSource.h +++ b/src/bun.js/bindings/webcore/JSReadableStreamSource.h @@ -66,6 +66,7 @@ class JSReadableStreamSource : public JSDOMWrapper { // Custom functions JSC::JSValue start(JSC::JSGlobalObject&, JSC::CallFrame&, Ref&&); JSC::JSValue pull(JSC::JSGlobalObject&, JSC::CallFrame&, Ref&&); + protected: JSReadableStreamSource(JSC::Structure*, JSDOMGlobalObject&, Ref&&); diff --git a/src/bun.js/bindings/webcore/ReadableStream.cpp b/src/bun.js/bindings/webcore/ReadableStream.cpp index d2973635433f6b..a6f22d0f555251 100644 --- a/src/bun.js/bindings/webcore/ReadableStream.cpp +++ b/src/bun.js/bindings/webcore/ReadableStream.cpp @@ -232,16 +232,12 @@ bool ReadableStream::isLocked(JSGlobalObject* globalObject, JSReadableStream* re bool ReadableStream::isDisturbed(JSGlobalObject* globalObject, JSReadableStream* readableStream) { - auto clientData = WebCore::clientData(globalObject->vm()); - auto& privateName = clientData->builtinNames().disturbedPrivateName(); - return readableStream->getDirect(globalObject->vm(), privateName).isTrue(); + return readableStream->disturbed(); } bool ReadableStream::isDisturbed() const { - auto clientData = WebCore::clientData(globalObject()->vm()); - auto& privateName = clientData->builtinNames().disturbedPrivateName(); - return readableStream()->getDirect(globalObject()->vm(), privateName).isTrue(); + return readableStream()->disturbed(); } } diff --git a/src/bun.js/bindings/webcore/ReadableStreamSource.cpp b/src/bun.js/bindings/webcore/ReadableStreamSource.cpp index f969c84b1794ab..e0537b418872b7 100644 --- a/src/bun.js/bindings/webcore/ReadableStreamSource.cpp +++ b/src/bun.js/bindings/webcore/ReadableStreamSource.cpp @@ -25,7 +25,6 @@ #include "config.h" #include "ReadableStreamSource.h" - namespace WebCore { ReadableStreamSource::~ReadableStreamSource() = default; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 424bab6363bf8c..14389cec907626 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -2,6 +2,7 @@ const std = @import("std"); const JSC = @import("root").bun.JSC; const JSGlobalObject = JSC.JSGlobalObject; const VirtualMachine = JSC.VirtualMachine; +const Allocator = std.mem.Allocator; const Lock = @import("../lock.zig").Lock; const bun = @import("root").bun; const Environment = bun.Environment; @@ -348,16 +349,20 @@ const Futimes = JSC.Node.Async.futimes; const Lchmod = JSC.Node.Async.lchmod; const Lchown = JSC.Node.Async.lchown; const Unlink = JSC.Node.Async.unlink; -const WaitPidResultTask = JSC.Subprocess.WaiterThread.WaitPidResultTask; const ShellGlobTask = bun.shell.interpret.Interpreter.Expansion.ShellGlobTask; const ShellRmTask = bun.shell.Interpreter.Builtin.Rm.ShellRmTask; const ShellRmDirTask = bun.shell.Interpreter.Builtin.Rm.ShellRmTask.DirTask; -const ShellRmDirTaskMini = bun.shell.InterpreterMini.Builtin.Rm.ShellRmTask.DirTask; const ShellLsTask = bun.shell.Interpreter.Builtin.Ls.ShellLsTask; const ShellMvCheckTargetTask = bun.shell.Interpreter.Builtin.Mv.ShellMvCheckTargetTask; const ShellMvBatchedTask = bun.shell.Interpreter.Builtin.Mv.ShellMvBatchedTask; -const ShellSubprocessResultTask = JSC.Subprocess.WaiterThread.ShellSubprocessQueue.ResultTask; +const ShellMkdirTask = bun.shell.Interpreter.Builtin.Mkdir.ShellMkdirTask; +const ShellTouchTask = bun.shell.Interpreter.Builtin.Touch.ShellTouchTask; +// const ShellIOReaderAsyncDeinit = bun.shell.Interpreter.IOReader.AsyncDeinit; +const ShellIOReaderAsyncDeinit = bun.shell.Interpreter.AsyncDeinit; +const ShellIOWriterAsyncDeinit = bun.shell.Interpreter.AsyncDeinitWriter; const TimerReference = JSC.BunTimer.Timeout.TimerReference; +const ProcessWaiterThreadTask = if (Environment.isPosix) bun.spawn.WaiterThread.ProcessQueue.ResultTask else opaque {}; +const ProcessMiniEventLoopWaiterThreadTask = if (Environment.isPosix) bun.spawn.WaiterThread.ProcessMiniEventLoopQueue.ResultTask else opaque {}; // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ FetchTasklet, @@ -368,6 +373,8 @@ pub const Task = TaggedPointerUnion(.{ WriteFileTask, AnyTask, ManagedTask, + ShellIOReaderAsyncDeinit, + ShellIOWriterAsyncDeinit, napi_async_work, ThreadSafeFunction, CppTask, @@ -416,18 +423,17 @@ pub const Task = TaggedPointerUnion(.{ Lchmod, Lchown, Unlink, - // WaitPidResultTask, - // These need to be referenced like this so they both don't become `WaitPidResultTask` - JSC.Subprocess.WaiterThread.WaitPidResultTask, - ShellSubprocessResultTask, ShellGlobTask, ShellRmTask, ShellRmDirTask, - ShellRmDirTaskMini, ShellMvCheckTargetTask, ShellMvBatchedTask, ShellLsTask, + ShellMkdirTask, + ShellTouchTask, TimerReference, + + ProcessWaiterThreadTask, }); const UnboundedQueue = @import("./unbounded_queue.zig").UnboundedQueue; pub const ConcurrentTask = struct { @@ -634,7 +640,70 @@ comptime { } } -pub const DeferredRepeatingTask = *const (fn (*anyopaque) bool); +/// Sometimes, you have work that will be scheduled, cancelled, and rescheduled multiple times +/// The order of that work may not particularly matter. +/// +/// An example of this is when writing to a file or network socket. +/// +/// You want to balance: +/// 1) Writing as much as possible to the file/socket in as few system calls as possible +/// 2) Writing to the file/socket as soon as possible +/// +/// That is a scheduling problem. How do you decide when to write to the file/socket? Developers +/// don't want to remember to call `flush` every time they write to a file/socket, but we don't +/// want them to have to think about buffering or not buffering either. +/// +/// Our answer to this is the DeferredTaskQueue. +/// +/// When you call write() when sending a streaming HTTP response, we don't actually write it immediately +/// by default. Instead, we wait until the end of the microtask queue to write it, unless either: +/// +/// - The buffer is full +/// - The developer calls `flush` manually +/// +/// But that means every time you call .write(), we have to check not only if the buffer is full, but also if +/// it previously had scheduled a write to the file/socket. So we use an ArrayHashMap to keep track of the +/// list of pointers which have a deferred task scheduled. +/// +/// The DeferredTaskQueue is drained after the microtask queue, but before other tasks are executed. This avoids re-entrancy +/// issues with the event loop. +pub const DeferredTaskQueue = struct { + pub const DeferredRepeatingTask = *const (fn (*anyopaque) bool); + + map: std.AutoArrayHashMapUnmanaged(?*anyopaque, DeferredRepeatingTask) = .{}, + + pub fn postTask(this: *DeferredTaskQueue, ctx: ?*anyopaque, task: DeferredRepeatingTask) bool { + const existing = this.map.getOrPutValue(bun.default_allocator, ctx, task) catch bun.outOfMemory(); + return existing.found_existing; + } + + pub fn unregisterTask(this: *DeferredTaskQueue, ctx: ?*anyopaque) bool { + return this.map.swapRemove(ctx); + } + + pub fn run(this: *DeferredTaskQueue) void { + var i: usize = 0; + var last = this.map.count(); + while (i < last) { + const key = this.map.keys()[i] orelse { + this.map.swapRemoveAt(i); + last = this.map.count(); + continue; + }; + + if (!this.map.values()[i](key)) { + this.map.swapRemoveAt(i); + last = this.map.count(); + } else { + i += 1; + } + } + } + + pub fn deinit(this: *DeferredTaskQueue) void { + this.map.deinit(bun.default_allocator); + } +}; pub const EventLoop = struct { tasks: Queue = undefined, @@ -654,9 +723,8 @@ pub const EventLoop = struct { global: *JSGlobalObject = undefined, virtual_machine: *JSC.VirtualMachine = undefined, waker: ?Waker = null, - defer_count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), forever_timer: ?*uws.Timer = null, - deferred_microtask_map: std.AutoArrayHashMapUnmanaged(?*anyopaque, DeferredRepeatingTask) = .{}, + deferred_tasks: DeferredTaskQueue = .{}, uws_loop: if (Environment.isWindows) *uws.Loop else void = undefined, timer_reference_pool: ?*bun.JSC.BunTimer.Timeout.TimerReference.Pool = null, @@ -725,6 +793,10 @@ pub const EventLoop = struct { }; } + pub fn pipeReadBuffer(this: *const EventLoop) []u8 { + return this.virtual_machine.rareData().pipeReadBuffer(); + } + pub const Queue = std.fifo.LinearFifo(Task, .Dynamic); const log = bun.Output.scoped(.EventLoop, false); @@ -739,7 +811,7 @@ pub const EventLoop = struct { JSC.markBinding(@src()); JSC__JSGlobalObject__drainMicrotasks(globalObject); - this.drainDeferredTasks(); + this.deferred_tasks.run(); if (comptime bun.Environment.isDebug) { this.debug.drain_microtasks_count_outside_tick_queue += @as(usize, @intFromBool(!this.debug.is_inside_tick_queue)); @@ -747,37 +819,10 @@ pub const EventLoop = struct { } pub fn drainMicrotasks(this: *EventLoop) void { + this.virtual_machine.jsc.releaseWeakRefs(); this.drainMicrotasksWithGlobal(this.global); } - pub fn registerDeferredTask(this: *EventLoop, ctx: ?*anyopaque, task: DeferredRepeatingTask) bool { - const existing = this.deferred_microtask_map.getOrPutValue(this.virtual_machine.allocator, ctx, task) catch unreachable; - return existing.found_existing; - } - - pub fn unregisterDeferredTask(this: *EventLoop, ctx: ?*anyopaque) bool { - return this.deferred_microtask_map.swapRemove(ctx); - } - - fn drainDeferredTasks(this: *EventLoop) void { - var i: usize = 0; - var last = this.deferred_microtask_map.count(); - while (i < last) { - const key = this.deferred_microtask_map.keys()[i] orelse { - this.deferred_microtask_map.swapRemoveAt(i); - last = this.deferred_microtask_map.count(); - continue; - }; - - if (!this.deferred_microtask_map.values()[i](key)) { - this.deferred_microtask_map.swapRemoveAt(i); - last = this.deferred_microtask_map.count(); - } else { - i += 1; - } - } - } - /// When you call a JavaScript function from outside the event loop task /// queue /// @@ -837,6 +882,26 @@ pub const EventLoop = struct { while (@field(this, queue_name).readItem()) |task| { defer counter += 1; switch (task.tag()) { + @field(Task.Tag, typeBaseName(@typeName(ShellIOWriterAsyncDeinit))) => { + var shell_ls_task: *ShellIOWriterAsyncDeinit = task.get(ShellIOWriterAsyncDeinit).?; + shell_ls_task.runFromMainThread(); + // shell_ls_task.deinit(); + }, + @field(Task.Tag, typeBaseName(@typeName(ShellIOReaderAsyncDeinit))) => { + var shell_ls_task: *ShellIOReaderAsyncDeinit = task.get(ShellIOReaderAsyncDeinit).?; + shell_ls_task.runFromMainThread(); + // shell_ls_task.deinit(); + }, + @field(Task.Tag, typeBaseName(@typeName(ShellTouchTask))) => { + var shell_ls_task: *ShellTouchTask = task.get(ShellTouchTask).?; + shell_ls_task.runFromMainThread(); + // shell_ls_task.deinit(); + }, + @field(Task.Tag, typeBaseName(@typeName(ShellMkdirTask))) => { + var shell_ls_task: *ShellMkdirTask = task.get(ShellMkdirTask).?; + shell_ls_task.runFromMainThread(); + // shell_ls_task.deinit(); + }, @field(Task.Tag, typeBaseName(@typeName(ShellLsTask))) => { var shell_ls_task: *ShellLsTask = task.get(ShellLsTask).?; shell_ls_task.runFromMainThread(); @@ -860,11 +925,6 @@ pub const EventLoop = struct { shell_rm_task.runFromMainThread(); // shell_rm_task.deinit(); }, - @field(Task.Tag, typeBaseName(@typeName(ShellRmDirTaskMini))) => { - var shell_rm_task: *ShellRmDirTaskMini = task.get(ShellRmDirTaskMini).?; - shell_rm_task.runFromMainThread(); - // shell_rm_task.deinit(); - }, @field(Task.Tag, typeBaseName(@typeName(ShellGlobTask))) => { var shell_glob_task: *ShellGlobTask = task.get(ShellGlobTask).?; shell_glob_task.runFromMainThread(); @@ -1106,18 +1166,13 @@ pub const EventLoop = struct { var any: *Unlink = task.get(Unlink).?; any.runFromJSThread(); }, - @field(Task.Tag, typeBaseName(@typeName(WaitPidResultTask))) => { - var any: *WaitPidResultTask = task.get(WaitPidResultTask).?; - any.runFromJSThread(); - }, - @field(Task.Tag, typeBaseName(@typeName(ShellSubprocessResultTask))) => { - var any: *ShellSubprocessResultTask = task.get(ShellSubprocessResultTask).?; + @field(Task.Tag, typeBaseName(@typeName(ProcessWaiterThreadTask))) => { + bun.markPosixOnly(); + var any: *ProcessWaiterThreadTask = task.get(ProcessWaiterThreadTask).?; any.runFromJSThread(); }, @field(Task.Tag, typeBaseName(@typeName(TimerReference))) => { - if (Environment.isWindows) { - @panic("This should not be reachable on Windows"); - } + bun.markPosixOnly(); var any: *TimerReference = task.get(TimerReference).?; any.runFromJSThread(); }, @@ -1223,9 +1278,17 @@ pub const EventLoop = struct { if (loop.isActive()) { this.processGCTimer(); + var event_loop_sleep_timer = if (comptime Environment.isDebug) std.time.Timer.start() catch unreachable else {}; loop.tick(); + + if (comptime Environment.isDebug) { + log("tick {}", .{bun.fmt.fmtDuration(event_loop_sleep_timer.read())}); + } } else { loop.tickWithoutIdle(); + if (comptime Environment.isDebug) { + log("tickWithoutIdle", .{}); + } } this.flushImmediateQueue(); @@ -1463,7 +1526,7 @@ pub const EventLoop = struct { JSC.markBinding(@src()); if (this.virtual_machine.event_loop_handle == null) { if (comptime Environment.isWindows) { - this.uws_loop = bun.uws.Loop.init(); + this.uws_loop = bun.uws.Loop.get(); this.virtual_machine.event_loop_handle = Async.Loop.get(); } else { this.virtual_machine.event_loop_handle = bun.Async.Loop.get(); @@ -1584,6 +1647,13 @@ pub const EventLoopKind = enum { js, mini, + pub fn Type(comptime this: EventLoopKind) type { + return switch (this) { + .js => EventLoop, + .mini => MiniEventLoop, + }; + } + pub fn refType(comptime this: EventLoopKind) type { return switch (this) { .js => *JSC.VirtualMachine, @@ -1599,13 +1669,10 @@ pub const EventLoopKind = enum { } }; -pub fn AbstractVM(inner: anytype) brk: { - if (@TypeOf(inner) == *JSC.VirtualMachine) { - break :brk JsVM; - } else if (@TypeOf(inner) == *JSC.MiniEventLoop) { - break :brk MiniVM; - } - @compileError("Invalid event loop ctx: " ++ @typeName(@TypeOf(inner))); +pub fn AbstractVM(inner: anytype) switch (@TypeOf(inner)) { + *JSC.VirtualMachine => JsVM, + *JSC.MiniEventLoop => MiniVM, + else => @compileError("Invalid event loop ctx: " ++ @typeName(@TypeOf(inner))), } { if (comptime @TypeOf(inner) == *JSC.VirtualMachine) return JsVM.init(inner); if (comptime @TypeOf(inner) == *JSC.MiniEventLoop) return MiniVM.init(inner); @@ -1620,7 +1687,7 @@ pub fn AbstractVM(inner: anytype) brk: { pub const MiniEventLoop = struct { tasks: Queue, - concurrent_tasks: UnboundedQueue(AnyTaskWithExtraContext, .next) = .{}, + concurrent_tasks: ConcurrentTaskQueue = .{}, loop: *uws.Loop, allocator: std.mem.Allocator, file_polls_: ?*Async.FilePoll.Store = null, @@ -1628,10 +1695,16 @@ pub const MiniEventLoop = struct { top_level_dir: []const u8 = "", after_event_loop_callback_ctx: ?*anyopaque = null, after_event_loop_callback: ?JSC.OpaqueCallback = null, + pipe_read_buffer: ?*PipeReadBuffer = null, + const PipeReadBuffer = [256 * 1024]u8; + pub threadlocal var globalInitialized: bool = false; pub threadlocal var global: *MiniEventLoop = undefined; + pub const ConcurrentTaskQueue = UnboundedQueue(AnyTaskWithExtraContext, .next); + pub fn initGlobal(env: ?*bun.DotEnv.Loader) *MiniEventLoop { + if (globalInitialized) return global; const loop = MiniEventLoop.init(bun.default_allocator); global = bun.default_allocator.create(MiniEventLoop) catch bun.outOfMemory(); global.* = loop; @@ -1643,6 +1716,7 @@ pub const MiniEventLoop = struct { loader.* = bun.DotEnv.Loader.init(map, bun.default_allocator); break :env_loader loader; }; + globalInitialized = true; return global; } @@ -1659,6 +1733,13 @@ pub const MiniEventLoop = struct { bun.Output.flush(); } + pub fn pipeReadBuffer(this: *MiniEventLoop) []u8 { + return this.pipe_read_buffer orelse { + this.pipe_read_buffer = this.allocator.create(PipeReadBuffer) catch bun.outOfMemory(); + return this.pipe_read_buffer.?; + }; + } + pub fn onAfterEventLoop(this: *MiniEventLoop) void { if (this.after_event_loop_callback) |cb| { const ctx = this.after_event_loop_callback_ctx; @@ -1715,10 +1796,44 @@ pub const MiniEventLoop = struct { return this.tasks.count - start_count; } + pub fn tickOnce( + this: *MiniEventLoop, + context: *anyopaque, + ) void { + if (this.tickConcurrentWithCount() == 0 and this.tasks.count == 0) { + defer this.onAfterEventLoop(); + this.loop.inc(); + this.loop.tick(); + this.loop.dec(); + } + + while (this.tasks.readItem()) |task| { + task.run(context); + } + } + + pub fn tickWithoutIdle( + this: *MiniEventLoop, + context: *anyopaque, + ) void { + defer this.onAfterEventLoop(); + + while (true) { + _ = this.tickConcurrentWithCount(); + while (this.tasks.readItem()) |task| { + task.run(context); + } + + this.loop.tickWithoutIdle(); + + if (this.tasks.count == 0 and this.tickConcurrentWithCount() == 0) break; + } + } + pub fn tick( this: *MiniEventLoop, context: *anyopaque, - comptime isDone: fn (*anyopaque) bool, + comptime isDone: *const fn (*anyopaque) bool, ) void { while (!isDone(context)) { if (this.tickConcurrentWithCount() == 0 and this.tasks.count == 0) { @@ -1770,7 +1885,7 @@ pub const MiniEventLoop = struct { }; pub const AnyEventLoop = union(enum) { - jsc: *EventLoop, + js: *EventLoop, mini: MiniEventLoop, pub const Task = AnyTaskWithExtraContext; @@ -1779,7 +1894,39 @@ pub const AnyEventLoop = union(enum) { this: *AnyEventLoop, jsc: *EventLoop, ) void { - this.* = .{ .jsc = jsc }; + this.* = .{ .js = jsc }; + } + + pub fn wakeup(this: *AnyEventLoop) void { + this.loop().wakeup(); + } + + pub fn filePolls(this: *AnyEventLoop) *bun.Async.FilePoll.Store { + return switch (this.*) { + .js => this.js.virtual_machine.rareData().filePolls(this.js.virtual_machine), + .mini => this.mini.filePolls(), + }; + } + + pub fn putFilePoll(this: *AnyEventLoop, poll: *Async.FilePoll) void { + switch (this.*) { + .js => this.js.virtual_machine.rareData().filePolls(this.js.virtual_machine).put(poll, this.js.virtual_machine, poll.flags.contains(.was_ever_registered)), + .mini => this.mini.filePolls().put(poll, &this.mini, poll.flags.contains(.was_ever_registered)), + } + } + + pub fn loop(this: *AnyEventLoop) *uws.Loop { + return switch (this.*) { + .js => this.js.virtual_machine.uwsLoop(), + .mini => this.mini.loop, + }; + } + + pub fn pipeReadBuffer(this: *AnyEventLoop) []u8 { + return switch (this.*) { + .js => this.js.pipeReadBuffer(), + .mini => this.mini.pipeReadBuffer(), + }; } pub fn init( @@ -1790,16 +1937,33 @@ pub const AnyEventLoop = union(enum) { pub fn tick( this: *AnyEventLoop, - context: *anyopaque, - comptime isDone: fn (*anyopaque) bool, + context: anytype, + comptime isDone: *const fn (@TypeOf(context)) bool, ) void { switch (this.*) { - .jsc => { - this.jsc.tick(); - this.jsc.autoTick(); + .js => { + while (!isDone(context)) { + this.js.tick(); + this.js.autoTick(); + } }, .mini => { - this.mini.tick(context, isDone); + this.mini.tick(context, @ptrCast(isDone)); + }, + } + } + + pub fn tickOnce( + this: *AnyEventLoop, + context: anytype, + ) void { + switch (this.*) { + .js => { + this.js.tick(); + this.js.autoTickActive(); + }, + .mini => { + this.mini.tickWithoutIdle(context); }, } } @@ -1813,7 +1977,7 @@ pub const AnyEventLoop = union(enum) { comptime field: std.meta.FieldEnum(Context), ) void { switch (this.*) { - .jsc => { + .js => { unreachable; // TODO: // const TaskType = AnyTask.New(Context, Callback); // @field(ctx, field) = TaskType.init(ctx); @@ -1828,3 +1992,153 @@ pub const AnyEventLoop = union(enum) { } } }; + +pub const EventLoopHandle = union(enum) { + js: *JSC.EventLoop, + mini: *MiniEventLoop, + + pub fn cast(this: EventLoopHandle, comptime as: @Type(.EnumLiteral)) if (as == .js) *JSC.EventLoop else *MiniEventLoop { + if (as == .js) { + if (this != .js) @panic("Expected *JSC.EventLoop but got *MiniEventLoop"); + return this.js; + } + + if (as == .mini) { + if (this != .mini) @panic("Expected *MiniEventLoop but got *JSC.EventLoop"); + return this.js; + } + + @compileError("Invalid event loop kind " ++ @typeName(as)); + } + + pub fn enter(this: EventLoopHandle) void { + switch (this) { + .js => this.js.enter(), + .mini => {}, + } + } + + pub fn exit(this: EventLoopHandle) void { + switch (this) { + .js => this.js.exit(), + .mini => {}, + } + } + + pub fn init(context: anytype) EventLoopHandle { + const Context = @TypeOf(context); + return switch (Context) { + *JSC.VirtualMachine => .{ .js = context.eventLoop() }, + *JSC.EventLoop => .{ .js = context }, + *JSC.MiniEventLoop => .{ .mini = context }, + *AnyEventLoop => switch (context.*) { + .js => .{ .js = context.js }, + .mini => .{ .mini = &context.mini }, + }, + EventLoopHandle => context, + else => @compileError("Invalid context type for EventLoopHandle.init " ++ @typeName(Context)), + }; + } + + pub fn filePolls(this: EventLoopHandle) *bun.Async.FilePoll.Store { + return switch (this) { + .js => this.js.virtual_machine.rareData().filePolls(this.js.virtual_machine), + .mini => this.mini.filePolls(), + }; + } + + pub fn putFilePoll(this: *EventLoopHandle, poll: *Async.FilePoll) void { + switch (this.*) { + .js => this.js.virtual_machine.rareData().filePolls(this.js.virtual_machine).put(poll, this.js.virtual_machine, poll.flags.contains(.was_ever_registered)), + .mini => this.mini.filePolls().put(poll, &this.mini, poll.flags.contains(.was_ever_registered)), + } + } + + pub fn enqueueTaskConcurrent(this: EventLoopHandle, context: EventLoopTaskPtr) void { + switch (this) { + .js => { + this.js.enqueueTaskConcurrent(context.js); + }, + .mini => { + this.mini.enqueueTaskConcurrent(context.mini); + }, + } + } + + pub fn loop(this: EventLoopHandle) *bun.uws.Loop { + return switch (this) { + .js => this.js.usocketsLoop(), + .mini => this.mini.loop, + }; + } + + pub fn pipeReadBuffer(this: EventLoopHandle) []u8 { + return switch (this) { + .js => this.js.pipeReadBuffer(), + .mini => this.mini.pipeReadBuffer(), + }; + } + + pub const platformEventLoop = loop; + + pub fn ref(this: EventLoopHandle) void { + this.loop().ref(); + } + + pub fn unref(this: EventLoopHandle) void { + this.loop().unref(); + } + + pub inline fn createNullDelimitedEnvMap(this: @This(), alloc: Allocator) ![:null]?[*:0]u8 { + return switch (this) { + .js => this.js.virtual_machine.bundler.env.map.createNullDelimitedEnvMap(alloc), + .mini => this.mini.env.?.map.createNullDelimitedEnvMap(alloc), + }; + } + + pub inline fn allocator(this: EventLoopHandle) Allocator { + return switch (this) { + .js => this.js.virtual_machine.allocator, + .mini => this.mini.allocator, + }; + } + + pub inline fn topLevelDir(this: EventLoopHandle) []const u8 { + return switch (this) { + .js => this.js.virtual_machine.bundler.fs.top_level_dir, + .mini => this.mini.top_level_dir, + }; + } + + pub inline fn env(this: EventLoopHandle) *bun.DotEnv.Loader { + return switch (this) { + .js => this.js.virtual_machine.bundler.env, + .mini => this.mini.env.?, + }; + } +}; + +pub const EventLoopTask = union { + js: ConcurrentTask, + mini: JSC.AnyTaskWithExtraContext, + + pub fn init(comptime kind: @TypeOf(.EnumLiteral)) EventLoopTask { + switch (kind) { + .js => return .{ .js = ConcurrentTask{} }, + .mini => return .{ .mini = JSC.AnyTaskWithExtraContext{} }, + else => @compileError("Invalid kind: " ++ @typeName(kind)), + } + } + + pub fn fromEventLoop(loop: JSC.EventLoopHandle) EventLoopTask { + switch (loop) { + .js => return .{ .js = ConcurrentTask{} }, + .mini => return .{ .mini = JSC.AnyTaskWithExtraContext{} }, + } + } +}; + +pub const EventLoopTaskPtr = union { + js: *ConcurrentTask, + mini: *JSC.AnyTaskWithExtraContext, +}; diff --git a/src/bun.js/ipc.zig b/src/bun.js/ipc.zig index 9d8af127b7df31..4d4019dbd912a7 100644 --- a/src/bun.js/ipc.zig +++ b/src/bun.js/ipc.zig @@ -35,11 +35,6 @@ pub const IPCMessageType = enum(u8) { _, }; -pub const IPCBuffer = struct { - list: bun.ByteList = .{}, - cursor: u32 = 0, -}; - /// Given potentially unfinished buffer `data`, attempt to decode and process a message from it. /// Returns `NotEnoughBytes` if there werent enough bytes /// Returns `InvalidFormat` if the message was invalid, probably close the socket in this case @@ -94,14 +89,14 @@ pub fn decodeIPCMessage( pub const Socket = uws.NewSocketHandler(false); -pub const IPCData = struct { +pub const SocketIPCData = struct { socket: Socket, - incoming: bun.ByteList = .{}, // Maybe we should use IPCBuffer here as well - outgoing: IPCBuffer = .{}, + incoming: bun.ByteList = .{}, // Maybe we should use StreamBuffer here as well + outgoing: bun.io.StreamBuffer = .{}, has_written_version: if (Environment.allow_assert) u1 else u0 = 0, - pub fn writeVersionPacket(this: *IPCData) void { + pub fn writeVersionPacket(this: *SocketIPCData) void { if (Environment.allow_assert) { std.debug.assert(this.has_written_version == 0); } @@ -112,15 +107,14 @@ pub const IPCData = struct { const bytes = comptime std.mem.asBytes(&VersionPacket{}); const n = this.socket.write(bytes, false); if (n != bytes.len) { - var list = this.outgoing.list.listManaged(bun.default_allocator); - list.appendSlice(bytes) catch @panic("OOM"); + this.outgoing.write(bytes) catch bun.outOfMemory(); } if (Environment.allow_assert) { this.has_written_version = 1; } } - pub fn serializeAndSend(ipc_data: *IPCData, globalThis: *JSGlobalObject, value: JSValue) bool { + pub fn serializeAndSend(ipc_data: *SocketIPCData, globalThis: *JSGlobalObject, value: JSValue) bool { if (Environment.allow_assert) { std.debug.assert(ipc_data.has_written_version == 1); } @@ -132,21 +126,22 @@ pub const IPCData = struct { const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; - ipc_data.outgoing.list.ensureUnusedCapacity(bun.default_allocator, payload_length) catch @panic("OOM"); - const start_offset = ipc_data.outgoing.list.len; + ipc_data.outgoing.ensureUnusedCapacity(payload_length) catch bun.outOfMemory(); + //TODO: probably we should not direct access ipc_data.outgoing.list.items here + const start_offset = ipc_data.outgoing.list.items.len; - ipc_data.outgoing.list.writeTypeAsBytesAssumeCapacity(u8, @intFromEnum(IPCMessageType.SerializedMessage)); - ipc_data.outgoing.list.writeTypeAsBytesAssumeCapacity(u32, size); - ipc_data.outgoing.list.appendSliceAssumeCapacity(serialized.data); + ipc_data.outgoing.writeTypeAsBytesAssumeCapacity(u8, @intFromEnum(IPCMessageType.SerializedMessage)); + ipc_data.outgoing.writeTypeAsBytesAssumeCapacity(u32, size); + ipc_data.outgoing.writeAssumeCapacity(serialized.data); - std.debug.assert(ipc_data.outgoing.list.len == start_offset + payload_length); + std.debug.assert(ipc_data.outgoing.list.items.len == start_offset + payload_length); if (start_offset == 0) { std.debug.assert(ipc_data.outgoing.cursor == 0); - const n = ipc_data.socket.write(ipc_data.outgoing.list.ptr[start_offset..payload_length], false); + const n = ipc_data.socket.write(ipc_data.outgoing.list.items.ptr[start_offset..payload_length], false); if (n == payload_length) { - ipc_data.outgoing.list.len = 0; + ipc_data.outgoing.reset(); } else if (n > 0) { ipc_data.outgoing.cursor = @intCast(n); } @@ -156,17 +151,183 @@ pub const IPCData = struct { } }; -/// This type is shared between VirtualMachine and Subprocess for their respective IPC handlers -/// -/// `Context` must be a struct that implements this interface: -/// struct { -/// globalThis: ?*JSGlobalObject, -/// ipc: IPCData, -/// -/// fn handleIPCMessage(*Context, DecodedIPCMessage) void -/// fn handleIPCClose(*Context, Socket) void -/// } -pub fn NewIPCHandler(comptime Context: type) type { +const NamedPipeIPCData = struct { + const uv = bun.windows.libuv; + // we will use writer pipe as Duplex + writer: bun.io.StreamingWriter(NamedPipeIPCData, onWrite, onError, null, onClientClose) = .{}, + + incoming: bun.ByteList = .{}, // Maybe we should use IPCBuffer here as well + connected: bool = false, + has_written_version: if (Environment.allow_assert) u1 else u0 = 0, + connect_req: uv.uv_connect_t = std.mem.zeroes(uv.uv_connect_t), + server: ?*uv.Pipe = null, + onClose: ?CloseHandler = null, + const CloseHandler = struct { + callback: *const fn (*anyopaque) void, + context: *anyopaque, + }; + + fn onWrite(_: *NamedPipeIPCData, amount: usize, status: bun.io.WriteStatus) void { + log("onWrite {d} {}", .{ amount, status }); + } + + fn onError(_: *NamedPipeIPCData, err: bun.sys.Error) void { + log("Failed to write outgoing data {}", .{err}); + } + + fn onClientClose(this: *NamedPipeIPCData) void { + log("onClisentClose", .{}); + this.connected = false; + if (this.server) |server| { + // we must close the server too + server.close(onServerClose); + } else { + if (this.onClose) |handler| { + // deinit dont free the instance of IPCData we should call it before the onClose callback actually frees it + this.deinit(); + handler.callback(handler.context); + } + } + } + + fn onServerClose(pipe: *uv.Pipe) callconv(.C) void { + log("onServerClose", .{}); + const this = bun.cast(*NamedPipeIPCData, pipe.data); + this.server = null; + if (this.connected) { + // close and deinit client if connected + this.writer.close(); + return; + } + if (this.onClose) |handler| { + // deinit dont free the instance of IPCData we should call it before the onClose callback actually frees it + this.deinit(); + handler.callback(handler.context); + } + } + + pub fn writeVersionPacket(this: *NamedPipeIPCData) void { + if (Environment.allow_assert) { + std.debug.assert(this.has_written_version == 0); + } + const VersionPacket = extern struct { + type: IPCMessageType align(1) = .Version, + version: u32 align(1) = ipcVersion, + }; + + if (Environment.allow_assert) { + this.has_written_version = 1; + } + const bytes = comptime std.mem.asBytes(&VersionPacket{}); + if (this.connected) { + _ = this.writer.write(bytes); + } else { + // enqueue to be sent after connecting + this.writer.outgoing.write(bytes) catch bun.outOfMemory(); + } + } + + pub fn serializeAndSend(this: *NamedPipeIPCData, globalThis: *JSGlobalObject, value: JSValue) bool { + if (Environment.allow_assert) { + std.debug.assert(this.has_written_version == 1); + } + + const serialized = value.serialize(globalThis) orelse return false; + defer serialized.deinit(); + + const size: u32 = @intCast(serialized.data.len); + log("serializeAndSend {d}", .{size}); + + const payload_length: usize = @sizeOf(IPCMessageType) + @sizeOf(u32) + size; + + this.writer.outgoing.ensureUnusedCapacity(payload_length) catch @panic("OOM"); + const start_offset = this.writer.outgoing.list.items.len; + + this.writer.outgoing.writeTypeAsBytesAssumeCapacity(u8, @intFromEnum(IPCMessageType.SerializedMessage)); + this.writer.outgoing.writeTypeAsBytesAssumeCapacity(u32, size); + this.writer.outgoing.writeAssumeCapacity(serialized.data); + + std.debug.assert(this.writer.outgoing.list.items.len == start_offset + payload_length); + + if (start_offset == 0) { + std.debug.assert(this.writer.outgoing.cursor == 0); + if (this.connected) { + _ = this.writer.flush(); + } + } + + return true; + } + + pub fn close(this: *NamedPipeIPCData) void { + if (this.server) |server| { + server.close(onServerClose); + } else { + this.writer.close(); + } + } + + pub fn configureServer(this: *NamedPipeIPCData, comptime Context: type, instance: *Context, named_pipe: []const u8) JSC.Maybe(void) { + log("configureServer", .{}); + const ipc_pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory(); + this.server = ipc_pipe; + ipc_pipe.data = this; + if (ipc_pipe.init(uv.Loop.get(), false).asErr()) |err| { + bun.default_allocator.destroy(ipc_pipe); + this.server = null; + return .{ .err = err }; + } + ipc_pipe.data = @ptrCast(instance); + this.onClose = .{ + .callback = @ptrCast(&NewNamedPipeIPCHandler(Context).onClose), + .context = @ptrCast(instance), + }; + if (ipc_pipe.listenNamedPipe(named_pipe, 0, instance, NewNamedPipeIPCHandler(Context).onNewClientConnect).asErr()) |err| { + bun.default_allocator.destroy(ipc_pipe); + this.server = null; + return .{ .err = err }; + } + + ipc_pipe.setPendingInstancesCount(1); + + ipc_pipe.unref(); + + return .{ .result = {} }; + } + + pub fn configureClient(this: *NamedPipeIPCData, comptime Context: type, instance: *Context, named_pipe: []const u8) !void { + log("configureClient", .{}); + const ipc_pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory(); + ipc_pipe.init(uv.Loop.get(), true).unwrap() catch |err| { + bun.default_allocator.destroy(ipc_pipe); + return err; + }; + this.writer.startWithPipe(ipc_pipe).unwrap() catch |err| { + bun.default_allocator.destroy(ipc_pipe); + return err; + }; + this.connect_req.data = @ptrCast(instance); + this.onClose = .{ + .callback = @ptrCast(&NewNamedPipeIPCHandler(Context).onClose), + .context = @ptrCast(instance), + }; + try ipc_pipe.connect(&this.connect_req, named_pipe, instance, NewNamedPipeIPCHandler(Context).onConnect).unwrap(); + } + + fn deinit(this: *NamedPipeIPCData) void { + log("deinit", .{}); + this.writer.deinit(); + if (this.server) |server| { + this.server = null; + bun.default_allocator.destroy(server); + } + this.incoming.deinitWithAllocator(bun.default_allocator); + } +}; + +pub const IPCData = if (Environment.isWindows) NamedPipeIPCData else SocketIPCData; + +pub fn NewSocketIPCHandler(comptime Context: type) type { return struct { pub fn onOpen( _: *anyopaque, @@ -183,13 +344,13 @@ pub fn NewIPCHandler(comptime Context: type) type { pub fn onClose( this: *Context, - socket: Socket, + _: Socket, _: c_int, _: ?*anyopaque, ) void { // ?! does uSockets .close call onClose? log("onClose\n", .{}); - this.handleIPCClose(socket); + this.handleIPCClose(); } pub fn onData( @@ -208,7 +369,7 @@ pub fn NewIPCHandler(comptime Context: type) type { if (this.globalThis) |global| { break :brk global; } - this.handleIPCClose(socket); + this.handleIPCClose(); socket.close(0, null); return; }, @@ -221,13 +382,13 @@ pub fn NewIPCHandler(comptime Context: type) type { while (true) { const result = decodeIPCMessage(data, globalThis) catch |e| switch (e) { error.NotEnoughBytes => { - _ = this.ipc.incoming.write(bun.default_allocator, data) catch @panic("OOM"); + _ = this.ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); log("hit NotEnoughBytes", .{}); return; }, error.InvalidFormat => { Output.printErrorln("InvalidFormatError during IPC message handling", .{}); - this.handleIPCClose(socket); + this.handleIPCClose(); socket.close(0, null); return; }, @@ -243,7 +404,7 @@ pub fn NewIPCHandler(comptime Context: type) type { } } - _ = this.ipc.incoming.write(bun.default_allocator, data) catch @panic("OOM"); + _ = this.ipc.incoming.write(bun.default_allocator, data) catch bun.outOfMemory(); var slice = this.ipc.incoming.slice(); while (true) { @@ -257,7 +418,7 @@ pub fn NewIPCHandler(comptime Context: type) type { }, error.InvalidFormat => { Output.printErrorln("InvalidFormatError during IPC message handling", .{}); - this.handleIPCClose(socket); + this.handleIPCClose(); socket.close(0, null); return; }, @@ -279,16 +440,16 @@ pub fn NewIPCHandler(comptime Context: type) type { context: *Context, socket: Socket, ) void { - const to_write = context.ipc.outgoing.list.ptr[context.ipc.outgoing.cursor..context.ipc.outgoing.list.len]; + const to_write = context.ipc.outgoing.slice(); if (to_write.len == 0) { - context.ipc.outgoing.cursor = 0; - context.ipc.outgoing.list.len = 0; + context.ipc.outgoing.reset(); + context.ipc.outgoing.reset(); return; } const n = socket.write(to_write, false); if (n == to_write.len) { - context.ipc.outgoing.cursor = 0; - context.ipc.outgoing.list.len = 0; + context.ipc.outgoing.reset(); + context.ipc.outgoing.reset(); } else if (n > 0) { context.ipc.outgoing.cursor += @intCast(n); } @@ -318,3 +479,151 @@ pub fn NewIPCHandler(comptime Context: type) type { ) void {} }; } + +fn NewNamedPipeIPCHandler(comptime Context: type) type { + const uv = bun.windows.libuv; + return struct { + fn onReadAlloc(this: *Context, suggested_size: usize) []u8 { + var available = this.ipc.incoming.available(); + if (available.len < suggested_size) { + this.ipc.incoming.ensureUnusedCapacity(bun.default_allocator, suggested_size) catch bun.outOfMemory(); + available = this.ipc.incoming.available(); + } + log("onReadAlloc {d}", .{suggested_size}); + return available.ptr[0..suggested_size]; + } + + fn onReadError(this: *Context, err: bun.C.E) void { + log("onReadError {}", .{err}); + this.ipc.close(); + } + + fn onRead(this: *Context, buffer: []const u8) void { + log("onRead {d}", .{buffer.len}); + this.ipc.incoming.len += @as(u32, @truncate(buffer.len)); + var slice = this.ipc.incoming.slice(); + + std.debug.assert(this.ipc.incoming.len <= this.ipc.incoming.cap); + std.debug.assert(bun.isSliceInBuffer(buffer, this.ipc.incoming.allocatedSlice())); + + const globalThis = switch (@typeInfo(@TypeOf(this.globalThis))) { + .Pointer => this.globalThis, + .Optional => brk: { + if (this.globalThis) |global| { + break :brk global; + } + this.ipc.close(); + return; + }, + else => @panic("Unexpected globalThis type: " ++ @typeName(@TypeOf(this.globalThis))), + }; + while (true) { + const result = decodeIPCMessage(slice, globalThis) catch |e| switch (e) { + error.NotEnoughBytes => { + // copy the remaining bytes to the start of the buffer + bun.copy(u8, this.ipc.incoming.ptr[0..slice.len], slice); + this.ipc.incoming.len = @truncate(slice.len); + log("hit NotEnoughBytes2", .{}); + return; + }, + error.InvalidFormat => { + Output.printErrorln("InvalidFormatError during IPC message handling", .{}); + this.ipc.close(); + return; + }, + }; + + this.handleIPCMessage(result.message); + + if (result.bytes_consumed < slice.len) { + slice = slice[result.bytes_consumed..]; + } else { + // clear the buffer + this.ipc.incoming.len = 0; + return; + } + } + } + + pub fn onNewClientConnect(this: *Context, status: uv.ReturnCode) void { + log("onNewClientConnect {d}", .{status.int()}); + if (status.errEnum()) |_| { + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + } + const server = this.ipc.server orelse { + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + }; + var client = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory(); + client.init(uv.Loop.get(), true).unwrap() catch { + bun.default_allocator.destroy(client); + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + }; + + this.ipc.writer.startWithPipe(client).unwrap() catch { + bun.default_allocator.destroy(client); + Output.printErrorln("Failed to start IPC pipe", .{}); + return; + }; + + switch (server.accept(client)) { + .err => { + this.ipc.close(); + return; + }, + .result => { + this.ipc.connected = true; + client.readStart(this, onReadAlloc, onReadError, onRead).unwrap() catch { + this.ipc.close(); + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + }; + _ = this.ipc.writer.flush(); + }, + } + } + + pub fn onClose(this: *Context) void { + this.handleIPCClose(); + } + + fn onConnect(this: *Context, status: uv.ReturnCode) void { + log("onConnect {d}", .{status.int()}); + this.ipc.connected = true; + + if (status.errEnum()) |_| { + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + } + const stream = this.ipc.writer.getStream() orelse { + this.ipc.close(); + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + }; + + stream.readStart(this, onReadAlloc, onReadError, onRead).unwrap() catch { + this.ipc.close(); + Output.printErrorln("Failed to connect IPC pipe", .{}); + return; + }; + _ = this.ipc.writer.flush(); + } + }; +} + +/// This type is shared between VirtualMachine and Subprocess for their respective IPC handlers +/// +/// `Context` must be a struct that implements this interface: +/// struct { +/// globalThis: ?*JSGlobalObject, +/// ipc: IPCData, +/// +/// fn handleIPCMessage(*Context, DecodedIPCMessage) void +/// fn handleIPCClose(*Context) void +/// } +pub fn NewIPCHandler(comptime Context: type) type { + const IPCHandler = if (Environment.isWindows) NewNamedPipeIPCHandler else NewSocketIPCHandler; + return IPCHandler(Context); +} diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 05d3c1a35569f2..dedd026a4ffde0 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -115,6 +115,8 @@ const SourceMap = @import("../sourcemap/sourcemap.zig"); const ParsedSourceMap = SourceMap.Mapping.ParsedSourceMap; const MappingList = SourceMap.Mapping.List; +const uv = bun.windows.libuv; + pub const SavedSourceMap = struct { pub const vlq_offset = 24; @@ -506,7 +508,7 @@ pub const VirtualMachine = struct { hide_bun_stackframes: bool = true, is_printing_plugin: bool = false, - + is_shutting_down: bool = false, plugin_runner: ?PluginRunner = null, is_main_thread: bool = false, last_reported_error_for_dedupe: JSValue = .zero, @@ -625,6 +627,10 @@ pub const VirtualMachine = struct { return this.debugger != null; } + pub inline fn isShuttingDown(this: *const VirtualMachine) bool { + return this.is_shutting_down; + } + const VMHolder = struct { pub threadlocal var vm: ?*VirtualMachine = null; }; @@ -753,7 +759,9 @@ pub const VirtualMachine = struct { } if (map.map.fetchSwapRemove("BUN_INTERNAL_IPC_FD")) |kv| { - if (std.fmt.parseInt(i32, kv.value.value, 10) catch null) |fd| { + if (Environment.isWindows) { + this.initIPCInstance(kv.value.value); + } else if (std.fmt.parseInt(i32, kv.value.value, 10) catch null) |fd| { this.initIPCInstance(bun.toFD(fd)); } else { Output.printErrorln("Failed to parse BUN_INTERNAL_IPC_FD", .{}); @@ -765,7 +773,7 @@ pub const VirtualMachine = struct { // lookups on start for obscure flags which we do not want others to // depend on. if (map.get("BUN_FEATURE_FLAG_FORCE_WAITER_THREAD") != null) { - JSC.Subprocess.WaiterThread.setShouldUseWaiterThread(); + bun.spawn.WaiterThread.setShouldUseWaiterThread(); } if (strings.eqlComptime(gc_level, "1")) { @@ -1605,7 +1613,11 @@ pub const VirtualMachine = struct { } } - defer jsc_vm.module_loader.resetArena(jsc_vm); + // .print_source, which is used by exceptions avoids duplicating the entire source code + // but that means we have to be careful of the lifetime of the source code + // so we only want to reset the arena once its done freeing it. + defer if (flags != .print_source) jsc_vm.module_loader.resetArena(jsc_vm); + errdefer if (flags == .print_source) jsc_vm.module_loader.resetArena(jsc_vm); return try ModuleLoader.transpileSourceCode( jsc_vm, @@ -2401,7 +2413,7 @@ pub const VirtualMachine = struct { if (exception) |exception_| { var holder = ZigException.Holder.init(); var zig_exception: *ZigException = holder.zigException(); - defer zig_exception.deinit(); + holder.deinit(this); exception_.getStackTrace(&zig_exception.stack); if (zig_exception.stack.frames_len > 0) { if (allow_ansi_color) { @@ -2609,12 +2621,7 @@ pub const VirtualMachine = struct { } } - pub fn remapZigException( - this: *VirtualMachine, - exception: *ZigException, - error_instance: JSValue, - exception_list: ?*ExceptionList, - ) void { + pub fn remapZigException(this: *VirtualMachine, exception: *ZigException, error_instance: JSValue, exception_list: ?*ExceptionList, must_reset_parser_arena_later: *bool) void { error_instance.toZigException(this.global, exception); // defer this so that it copies correctly defer { @@ -2711,6 +2718,7 @@ pub const VirtualMachine = struct { if (mapping_) |mapping| { var log = logger.Log.init(default_allocator); var original_source = fetchWithoutOnLoadPlugins(this, this.global, top.source_url, bun.String.empty, &log, .print_source) catch return; + must_reset_parser_arena_later.* = true; const code = original_source.source_code.toUTF8(bun.default_allocator); defer code.deinit(); @@ -2740,6 +2748,8 @@ pub const VirtualMachine = struct { lines = lines[0..@min(@as(usize, lines.len), source_lines.len)]; var current_line_number: i32 = @intCast(last_line); for (lines, source_lines[0..lines.len], source_line_numbers[0..lines.len]) |line, *line_dest, *line_number| { + // To minimize duplicate allocations, we use the same slice as above + // it should virtually always be UTF-8 and thus not cloned line_dest.* = String.init(line); line_number.* = current_line_number; current_line_number -= 1; @@ -2777,8 +2787,8 @@ pub const VirtualMachine = struct { pub fn printErrorInstance(this: *VirtualMachine, error_instance: JSValue, exception_list: ?*ExceptionList, comptime Writer: type, writer: Writer, comptime allow_ansi_color: bool, comptime allow_side_effects: bool) anyerror!void { var exception_holder = ZigException.Holder.init(); var exception = exception_holder.zigException(); - defer exception_holder.deinit(); - this.remapZigException(exception, error_instance, exception_list); + defer exception_holder.deinit(this); + this.remapZigException(exception, error_instance, exception_list, &exception_holder.need_to_clear_parser_arena_on_deinit); const prev_had_errors = this.had_errors; this.had_errors = true; defer this.had_errors = prev_had_errors; @@ -2919,7 +2929,7 @@ pub const VirtualMachine = struct { if (error_instance != .zero and error_instance.isCell() and error_instance.jsType().canGet()) { inline for (extra_fields) |field| { - if (error_instance.getTruthy(this.global, field)) |value| { + if (error_instance.getTruthyComptime(this.global, field)) |value| { const kind = value.jsType(); if (kind.isStringLike()) { if (value.toStringOrNull(this.global)) |str| { @@ -3150,9 +3160,11 @@ pub const VirtualMachine = struct { pub const IPCInstance = struct { globalThis: ?*JSGlobalObject, - uws_context: *uws.SocketContext, + context: if (Environment.isPosix) *uws.SocketContext else u0, ipc: IPC.IPCData, + pub usingnamespace bun.New(@This()); + pub fn handleIPCMessage( this: *IPCInstance, message: IPC.DecodedIPCMessage, @@ -3173,36 +3185,54 @@ pub const VirtualMachine = struct { } } - pub fn handleIPCClose(this: *IPCInstance, _: IPC.Socket) void { + pub fn handleIPCClose(this: *IPCInstance) void { JSC.markBinding(@src()); if (this.globalThis) |global| { var vm = global.bunVM(); vm.ipc = null; Process__emitDisconnectEvent(global); } - uws.us_socket_context_free(0, this.uws_context); - bun.default_allocator.destroy(this); + if (Environment.isPosix) { + uws.us_socket_context_free(0, this.context); + } + this.destroy(); } pub const Handlers = IPC.NewIPCHandler(IPCInstance); }; - pub fn initIPCInstance(this: *VirtualMachine, fd: bun.FileDescriptor) void { + const IPCInfoType = if (Environment.isWindows) []const u8 else bun.FileDescriptor; + pub fn initIPCInstance(this: *VirtualMachine, info: IPCInfoType) void { if (Environment.isWindows) { - Output.warn("IPC is not supported on Windows", .{}); + var instance = IPCInstance.new(.{ + .globalThis = this.global, + .context = 0, + .ipc = .{}, + }); + instance.ipc.configureClient(IPCInstance, instance, info) catch { + instance.destroy(); + Output.printErrorln("Unable to start IPC pipe", .{}); + return; + }; + + this.ipc = instance; + instance.ipc.writeVersionPacket(); return; } this.event_loop.ensureWaker(); const context = uws.us_create_socket_context(0, this.event_loop_handle.?, @sizeOf(usize), .{}).?; IPC.Socket.configure(context, true, *IPCInstance, IPCInstance.Handlers); - var instance = bun.default_allocator.create(IPCInstance) catch @panic("OOM"); - instance.* = .{ + var instance = IPCInstance.new(.{ .globalThis = this.global, - .uws_context = context, + .context = context, .ipc = undefined, + }); + const socket = IPC.Socket.fromFd(context, info, IPCInstance, instance, null) orelse { + instance.destroy(); + Output.printErrorln("Unable to start IPC socket", .{}); + return; }; - const socket = IPC.Socket.fromFd(context, fd, IPCInstance, instance, null) orelse @panic("Unable to start IPC"); socket.setTimeout(0); instance.ipc = .{ .socket = socket }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 7b5affdd84550b..ec3fea4cab4f47 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1473,10 +1473,14 @@ pub const ModuleLoader = struct { defer { if (give_back_arena) { if (jsc_vm.module_loader.transpile_source_code_arena == null) { - if (jsc_vm.smol) { - _ = arena_.?.reset(.free_all); - } else { - _ = arena_.?.reset(.{ .retain_with_limit = 8 * 1024 * 1024 }); + // when .print_source is used + // caller is responsible for freeing the arena + if (flags != .print_source) { + if (jsc_vm.smol) { + _ = arena_.?.reset(.free_all); + } else { + _ = arena_.?.reset(.{ .retain_with_limit = 8 * 1024 * 1024 }); + } } jsc_vm.module_loader.transpile_source_code_arena = arena_; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index 54aa660e2141eb..c0c9b8bc466968 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -4275,7 +4275,7 @@ pub const NodeFS = struct { return if (args.recursive) mkdirRecursive(this, args, flavor) else mkdirNonRecursive(this, args, flavor); } // Node doesn't absolute the path so we don't have to either - fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { + pub fn mkdirNonRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { _ = flavor; const path = args.path.sliceZ(&this.sync_error_buf); @@ -4285,8 +4285,18 @@ pub const NodeFS = struct { }; } - // TODO: verify this works correctly with unicode codepoints + pub const MkdirDummyVTable = struct { + pub fn onCreateDir(_: @This(), _: bun.OSPathSliceZ) void { + return; + } + }; + pub fn mkdirRecursive(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor) Maybe(Return.Mkdir) { + return mkdirRecursiveImpl(this, args, flavor, MkdirDummyVTable, .{}); + } + + // TODO: verify this works correctly with unicode codepoints + pub fn mkdirRecursiveImpl(this: *NodeFS, args: Arguments.Mkdir, comptime flavor: Flavor, comptime Ctx: type, ctx: Ctx) Maybe(Return.Mkdir) { _ = flavor; var buf: bun.OSPathBuffer = undefined; const path: bun.OSPathSliceZ = if (!Environment.isWindows) @@ -4306,7 +4316,7 @@ pub const NodeFS = struct { }; // TODO: remove and make it always a comptime argument return switch (args.always_return_none) { - inline else => |always_return_none| this.mkdirRecursiveOSPath(path, args.mode, !always_return_none), + inline else => |always_return_none| this.mkdirRecursiveOSPathImpl(Ctx, ctx, path, args.mode, !always_return_none), }; } @@ -4318,6 +4328,24 @@ pub const NodeFS = struct { } pub fn mkdirRecursiveOSPath(this: *NodeFS, path: bun.OSPathSliceZ, mode: Mode, comptime return_path: bool) Maybe(Return.Mkdir) { + return mkdirRecursiveOSPathImpl(this, MkdirDummyVTable, .{}, path, mode, return_path); + } + + pub fn mkdirRecursiveOSPathImpl( + this: *NodeFS, + comptime Ctx: type, + ctx: Ctx, + path: bun.OSPathSliceZ, + mode: Mode, + comptime return_path: bool, + ) Maybe(Return.Mkdir) { + const VTable = struct { + pub fn onCreateDir(c: Ctx, dirpath: bun.OSPathSliceZ) void { + c.onCreateDir(dirpath); + return; + } + }; + const Char = bun.OSPathChar; const len = @as(u16, @truncate(path.len)); @@ -4337,6 +4365,7 @@ pub const NodeFS = struct { } }, .result => { + VTable.onCreateDir(ctx, path); if (!return_path) { return .{ .result = .{ .none = {} } }; } @@ -4378,6 +4407,7 @@ pub const NodeFS = struct { } }, .result => { + VTable.onCreateDir(ctx, parent); // We found a parent that worked working_mem[i] = std.fs.path.sep; break; @@ -4408,6 +4438,7 @@ pub const NodeFS = struct { }, .result => { + VTable.onCreateDir(ctx, parent); working_mem[i] = std.fs.path.sep; }, } @@ -4433,6 +4464,7 @@ pub const NodeFS = struct { .result => {}, } + VTable.onCreateDir(ctx, working_mem[0..len :0]); if (!return_path) { return .{ .result = .{ .none = {} } }; } diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index dc54b3c87d93a7..6883fd49749fa7 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -136,6 +136,15 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { .result = std.mem.zeroes(ReturnType), }; + pub fn assert(this: @This()) ReturnType { + switch (this) { + .err => |err| { + bun.Output.panic("Unexpected error\n{}", .{err}); + }, + .result => |result| return result, + } + } + pub inline fn todo() @This() { if (Environment.allow_assert) { if (comptime ReturnType == void) { @@ -1228,7 +1237,7 @@ pub const PathOrFileDescriptor = union(Tag) { } switch (this) { .path => |p| try writer.writeAll(p.slice()), - .fd => |fd| try writer.print("{d}", .{fd}), + .fd => |fd| try writer.print("{}", .{fd}), } } diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index e23bd40bded504..1d09c9c4f220e8 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -45,6 +45,17 @@ node_fs_stat_watcher_scheduler: ?*StatWatcherScheduler = null, listening_sockets_for_watch_mode: std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, listening_sockets_for_watch_mode_lock: bun.Lock = bun.Lock.init(), +temp_pipe_read_buffer: ?*PipeReadBuffer = null, + +const PipeReadBuffer = [256 * 1024]u8; + +pub fn pipeReadBuffer(this: *RareData) *PipeReadBuffer { + return this.temp_pipe_read_buffer orelse { + this.temp_pipe_read_buffer = default_allocator.create(PipeReadBuffer) catch bun.outOfMemory(); + return this.temp_pipe_read_buffer.?; + }; +} + pub fn addListeningSocketForWatchMode(this: *RareData, socket: bun.FileDescriptor) void { this.listening_sockets_for_watch_mode_lock.lock(); defer this.listening_sockets_for_watch_mode_lock.unlock(); @@ -339,7 +350,7 @@ pub fn stdin(rare: *RareData) *Blob.Store { .pathlike = .{ .fd = fd, }, - .is_atty = std.os.isatty(bun.STDIN_FD.cast()), + .is_atty = if (bun.STDIN_FD.isValid()) std.os.isatty(bun.STDIN_FD.cast()) else false, .mode = mode, }, }, diff --git a/src/bun.js/test/diff_format.zig b/src/bun.js/test/diff_format.zig index ca7d03f57354a9..27e4bf78a04df3 100644 --- a/src/bun.js/test/diff_format.zig +++ b/src/bun.js/test/diff_format.zig @@ -102,7 +102,7 @@ pub const DiffFormatter = struct { .quote_strings = true, .max_depth = 100, }; - ConsoleObject.format( + ConsoleObject.format2( .Debug, this.globalObject, @as([*]const JSValue, @ptrCast(&received)), @@ -116,7 +116,7 @@ pub const DiffFormatter = struct { buffered_writer_.context = &expected_buf; - ConsoleObject.format( + ConsoleObject.format2( .Debug, this.globalObject, @as([*]const JSValue, @ptrCast(&this.expected)), diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index ef4dfb30871b4e..2d8e2138c5d438 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -1418,7 +1418,7 @@ pub const JestPrettyFormat = struct { comptime Output.prettyFmt("data: ", enable_ansi_colors), .{}, ); - const data = value.get(this.globalThis, "data").?; + const data = value.fastGet(this.globalThis, .data).?; const tag = Tag.get(data, this.globalThis); if (tag.cell.isStringLike()) { this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index 420ec2aa296e72..3f03db1fcb20c9 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -221,7 +221,7 @@ pub const WebWorker = struct { const Writer = @TypeOf(writer); // we buffer this because it'll almost always be < 4096 // when it's under 4096, we want to avoid the dynamic allocation - bun.JSC.ConsoleObject.format( + bun.JSC.ConsoleObject.format2( .Debug, globalObject, &[_]JSC.JSValue{error_instance}, @@ -362,6 +362,7 @@ pub const WebWorker = struct { var vm_to_deinit: ?*JSC.VirtualMachine = null; if (this.vm) |vm| { this.vm = null; + vm.is_shutting_down = true; vm.onExit(); exit_code = vm.exit_handler.exit_code; globalObject = vm.global; diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 78d9a737cff140..236140d8c4a552 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -244,7 +244,7 @@ pub const Prompt = struct { bun.Output.flush(); // 7. Pause while waiting for the user's response. - const reader = bun.buffered_stdin.reader(); + const reader = bun.Output.buffered_stdin.reader(); const first_byte = reader.readByte() catch { // 8. Let result be null if the user aborts, or otherwise the string diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 93ac7016771b40..6e630aa79aa7a9 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -1760,15 +1760,13 @@ pub const Blob = struct { .toSystemError(); self.opened_fd = invalid_fd; } else { - self.opened_fd = bun.toFD(@as(i32, @intCast(req.result.value))); - std.debug.assert(bun.uvfdcast(self.opened_fd) == req.result.value); + self.opened_fd = req.result.toFD(); } } Callback(self, self.opened_fd); } }; - // use real libuv async const rc = libuv.uv_fs_open( this.loop, &this.req, @@ -2030,7 +2028,8 @@ pub const Blob = struct { const promise = this.promise.swap(); const err_instance = err.toSystemError().toErrorInstance(globalThis); var event_loop = this.event_loop; - defer event_loop.drainMicrotasks(); + event_loop.enter(); + defer event_loop.exit(); this.deinit(); promise.reject(globalThis, err_instance); } @@ -2708,7 +2707,6 @@ pub const Blob = struct { max_size: SizeType = Blob.max_size, // milliseconds since ECMAScript epoch last_modified: JSC.JSTimeType = JSC.init_timestamp, - pipe: if (Environment.isWindows) libuv.uv_pipe_t else u0 = if (Environment.isWindows) std.mem.zeroes(libuv.uv_pipe_t) else 0, pub fn isSeekable(this: *const FileStore) ?bool { if (this.seekable) |seekable| { @@ -2943,8 +2941,7 @@ pub const Blob = struct { return JSValue.jsUndefined(); } - if (Environment.isWindows and !(store.data.file.is_atty orelse false)) { - // on Windows we use uv_pipe_t when not using TTY + if (Environment.isWindows) { const pathlike = store.data.file.pathlike; const fd: bun.FileDescriptor = if (pathlike == .fd) pathlike.fd else brk: { var file_path: [bun.MAX_PATH_BYTES]u8 = undefined; @@ -2957,45 +2954,19 @@ pub const Blob = struct { break :brk result; }, .err => |err| { - globalThis.throwInvalidArguments("Failed to create UVStreamSink: {}", .{err.getErrno()}); + globalThis.throwInvalidArguments("Failed to create FileSink: {}", .{err.getErrno()}); return JSValue.jsUndefined(); }, } unreachable; }; - var pipe_ptr = &(this.store.?.data.file.pipe); - if (store.data.file.pipe.loop == null) { - if (libuv.uv_pipe_init(libuv.Loop.get(), pipe_ptr, 0) != 0) { - pipe_ptr.loop = null; - globalThis.throwInvalidArguments("Failed to create UVStreamSink", .{}); - return JSValue.jsUndefined(); - } - const file_fd = bun.uvfdcast(fd); - if (libuv.uv_pipe_open(pipe_ptr, file_fd).errEnum()) |err| { - pipe_ptr.loop = null; - globalThis.throwInvalidArguments("Failed to create UVStreamSink: uv_pipe_open({d}) {}", .{ file_fd, err }); - return JSValue.jsUndefined(); - } - } - - var sink = JSC.WebCore.UVStreamSink.init(globalThis.allocator(), @ptrCast(pipe_ptr), null) catch |err| { - globalThis.throwInvalidArguments("Failed to create UVStreamSink: {s}", .{@errorName(err)}); - return JSValue.jsUndefined(); - }; + var sink = JSC.WebCore.FileSink.init(fd, this.globalThis.bunVM().eventLoop()); - var stream_start: JSC.WebCore.StreamStart = .{ - .UVStreamSink = {}, - }; - - if (arguments.len > 0 and arguments.ptr[0].isObject()) { - stream_start = JSC.WebCore.StreamStart.fromJSWithTag(globalThis, arguments[0], .UVStreamSink); - } - - switch (sink.start(stream_start)) { + switch (sink.writer.start(fd, false)) { .err => |err| { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - sink.finalize(); + sink.deref(); return JSC.JSValue.zero; }, @@ -3005,10 +2976,7 @@ pub const Blob = struct { return sink.toJS(globalThis); } - var sink = JSC.WebCore.FileSink.init(globalThis.allocator(), null) catch |err| { - globalThis.throwInvalidArguments("Failed to create FileSink: {s}", .{@errorName(err)}); - return JSValue.jsUndefined(); - }; + var sink = JSC.WebCore.FileSink.init(bun.invalid_fd, this.globalThis.bunVM().eventLoop()); const input_path: JSC.WebCore.PathOrFileDescriptor = brk: { if (store.data.file.pathlike == .fd) { @@ -3039,7 +3007,7 @@ pub const Blob = struct { switch (sink.start(stream_start)) { .err => |err| { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - sink.finalize(); + sink.deref(); return JSC.JSValue.zero; }, @@ -3590,9 +3558,9 @@ pub const Blob = struct { } pub fn toJS(this: *Blob, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - if (comptime Environment.allow_assert) { - std.debug.assert(this.allocator != null); - } + // if (comptime Environment.allow_assert) { + // std.debug.assert(this.allocator != null); + // } this.calculateEstimatedByteSize(); return Blob.toJSUnchecked(globalObject, this); @@ -3787,7 +3755,7 @@ pub const Blob = struct { if (this.store) |store| { if (store.data == .bytes) { const allocated_slice = store.data.bytes.allocatedSlice(); - if (bun.isSliceInBuffer(u8, buf, allocated_slice)) { + if (bun.isSliceInBuffer(buf, allocated_slice)) { if (bun.linux.memfd_allocator.from(store.data.bytes.allocator)) |allocator| { allocator.ref(); defer allocator.deref(); diff --git a/src/bun.js/webcore/blob/ReadFile.zig b/src/bun.js/webcore/blob/ReadFile.zig index c847f97444a935..ab6b8d2a5e701f 100644 --- a/src/bun.js/webcore/blob/ReadFile.zig +++ b/src/bun.js/webcore/blob/ReadFile.zig @@ -63,6 +63,7 @@ pub const ReadFile = struct { store: ?*Store = null, offset: SizeType = 0, max_length: SizeType = Blob.max_size, + total_size: SizeType = Blob.max_size, opened_fd: bun.FileDescriptor = invalid_fd, read_off: SizeType = 0, read_eof: bool = false, @@ -110,7 +111,7 @@ pub const ReadFile = struct { max_len: SizeType, ) !*ReadFile { if (Environment.isWindows) - @compileError("dont call this function on windows"); + @compileError("Do not call ReadFile.createWithCtx on Windows, see ReadFileUV"); const read_file = bun.new(ReadFile, ReadFile{ .file_store = store.data.file, @@ -212,10 +213,8 @@ pub const ReadFile = struct { pub fn doRead(this: *ReadFile, buffer: []u8, read_len: *usize, retry: *bool) bool { const result: JSC.Maybe(usize) = brk: { - if (comptime Environment.isPosix) { - if (std.os.S.ISSOCK(this.file_store.mode)) { - break :brk bun.sys.recv(this.opened_fd, buffer, std.os.SOCK.NONBLOCK); - } + if (std.os.S.ISSOCK(this.file_store.mode)) { + break :brk bun.sys.recvNonBlock(this.opened_fd, buffer); } break :brk bun.sys.read(this.opened_fd, buffer); @@ -272,7 +271,7 @@ pub const ReadFile = struct { return; } else if (this.store == null) { bun.destroy(this); - if (Environment.isDebug) @panic("assertion failure - store should not be null"); + if (Environment.allow_assert) @panic("assertion failure - store should not be null"); cb(cb_ctx, ResultType{ .err = SystemError{ .code = bun.String.static("INTERNAL_ERROR"), @@ -287,7 +286,6 @@ pub const ReadFile = struct { const buf = this.buffer.items; defer store.deref(); - const total_size = this.size; const system_error = this.system_error; bun.destroy(this); @@ -296,7 +294,7 @@ pub const ReadFile = struct { return; } - cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = total_size, .is_temporary = true } }); + cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = this.total_size, .is_temporary = true } }); } pub fn run(this: *ReadFile, task: *ReadFileTask) void { @@ -368,12 +366,10 @@ pub const ReadFile = struct { } this.could_block = !bun.isRegularFile(stat.mode); + this.total_size = @truncate(@as(SizeType, @intCast(@max(@as(i64, @intCast(stat.size)), 0)))); if (stat.size > 0 and !this.could_block) { - this.size = @min( - @as(SizeType, @truncate(@as(SizeType, @intCast(@max(@as(i64, @intCast(stat.size)), 0))))), - this.max_length, - ); + this.size = @min(this.total_size, this.max_length); // read up to 4k at a time if // they didn't explicitly set a size and we're reading from something that's not a regular file } else if (stat.size == 0 and this.could_block) { @@ -450,7 +446,6 @@ pub const ReadFile = struct { fn doReadLoop(this: *ReadFile) void { while (this.state.load(.Monotonic) == .running) { - // we hold a 64 KB stack buffer incase the amount of data to // be read is greater than the reported amount // @@ -469,9 +464,9 @@ pub const ReadFile = struct { if (read.ptr == &stack_buffer) { if (this.buffer.capacity == 0) { // We need to allocate a new buffer - // In this case, we want to use `initCapacity` so that it's an exact amount + // In this case, we want to use `ensureTotalCapacityPrecis` so that it's an exact amount // We want to avoid over-allocating incase it's a large amount of data sent in a single chunk followed by a 0 byte chunk. - this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, read.len) catch bun.outOfMemory(); + this.buffer.ensureTotalCapacityPrecise(bun.default_allocator, read.len) catch bun.outOfMemory(); } else { this.buffer.ensureUnusedCapacity(bun.default_allocator, read.len) catch bun.outOfMemory(); } @@ -556,19 +551,20 @@ pub const ReadFileUV = struct { store: *Store, offset: SizeType = 0, max_length: SizeType = Blob.max_size, + total_size: SizeType = Blob.max_size, opened_fd: bun.FileDescriptor = invalid_fd, read_len: SizeType = 0, read_off: SizeType = 0, read_eof: bool = false, size: SizeType = 0, - buffer: []u8 = &.{}, + buffer: std.ArrayListUnmanaged(u8) = .{}, system_error: ?JSC.SystemError = null, errno: ?anyerror = null, on_complete_data: *anyopaque = undefined, on_complete_fn: ReadFile.OnReadFileCallback, - could_block: bool = false, + is_regular_file: bool = false, - req: libuv.fs_t = libuv.fs_t.uninitialized, + req: libuv.fs_t = std.mem.zeroes(libuv.fs_t), pub fn start(loop: *libuv.Loop, store: *Store, off: SizeType, max_len: SizeType, comptime Handler: type, handler: *anyopaque) void { log("ReadFileUV.start", .{}); @@ -596,15 +592,13 @@ pub const ReadFileUV = struct { const cb = this.on_complete_fn; const cb_ctx = this.on_complete_data; - const buf = this.buffer; if (this.system_error) |err| { cb(cb_ctx, ReadFile.ResultType{ .err = err }); return; } - const size = this.size; - cb(cb_ctx, .{ .result = .{ .buf = buf, .total_size = size, .is_temporary = true } }); + cb(cb_ctx, .{ .result = .{ .buf = this.byte_store.slice(), .total_size = this.total_size, .is_temporary = true } }); } pub fn isAllowedToClose(this: *const ReadFileUV) bool { @@ -617,6 +611,7 @@ pub const ReadFileUV = struct { const needs_close = fd != bun.invalid_fd; this.size = @max(this.read_len, this.size); + this.total_size = @max(this.total_size, this.size); if (needs_close) { if (this.doClose(this.isAllowedToClose())) { @@ -636,6 +631,7 @@ pub const ReadFileUV = struct { } this.req.deinit(); + this.req.data = this; if (libuv.uv_fs_fstat(this.loop, &this.req, bun.uvfdcast(opened_fd), &onFileInitialStat).errEnum()) |errno| { this.errno = bun.errnoToZigErr(errno); @@ -643,6 +639,8 @@ pub const ReadFileUV = struct { this.onFinish(); return; } + + this.req.data = this; } fn onFileInitialStat(req: *libuv.fs_t) callconv(.C) void { @@ -657,64 +655,61 @@ pub const ReadFileUV = struct { } const stat = req.statbuf; + log("stat: {any}", .{stat}); // keep in sync with resolveSizeAndLastModified - { - if (this.store.data == .file) { - this.store.data.file.last_modified = JSC.toJSTime(stat.mtime().tv_sec, stat.mtime().tv_nsec); - } + if (this.store.data == .file) { + this.store.data.file.last_modified = JSC.toJSTime(stat.mtime().tv_sec, stat.mtime().tv_nsec); + } - if (bun.S.ISDIR(@intCast(stat.mode))) { - this.errno = error.EISDIR; - this.system_error = JSC.SystemError{ - .code = bun.String.static("EISDIR"), - .path = if (this.file_store.pathlike == .path) - bun.String.createUTF8(this.file_store.pathlike.path.slice()) - else - bun.String.empty, - .message = bun.String.static("Directories cannot be read like files"), - .syscall = bun.String.static("read"), - }; - this.onFinish(); - return; - } - this.could_block = !bun.isRegularFile(stat.mode); - - if (stat.size > 0 and !this.could_block) { - this.size = @min( - @as(SizeType, @truncate(@as(SizeType, @intCast(@max(@as(i64, @intCast(stat.size)), 0))))), - this.max_length, - ); - // read up to 4k at a time if - // they didn't explicitly set a size and we're reading from something that's not a regular file - } else if (stat.size == 0 and this.could_block) { - this.size = if (this.max_length == Blob.max_size) - 4096 + if (bun.S.ISDIR(@intCast(stat.mode))) { + this.errno = error.EISDIR; + this.system_error = JSC.SystemError{ + .code = bun.String.static("EISDIR"), + .path = if (this.file_store.pathlike == .path) + bun.String.createUTF8(this.file_store.pathlike.path.slice()) else - this.max_length; - } + bun.String.empty, + .message = bun.String.static("Directories cannot be read like files"), + .syscall = bun.String.static("read"), + }; + this.onFinish(); + return; + } + this.total_size = @truncate(@as(SizeType, @intCast(@max(@as(i64, @intCast(stat.size)), 0)))); + this.is_regular_file = bun.isRegularFile(stat.mode); - if (this.offset > 0) { - // We DO support offset in Bun.file() - switch (bun.sys.setFileOffset(this.opened_fd, this.offset)) { - // we ignore errors because it should continue to work even if its a pipe - .err, .result => {}, - } + log("is_regular_file: {}", .{this.is_regular_file}); + + if (stat.size > 0 and this.is_regular_file) { + this.size = @min(this.total_size, this.max_length); + } else if (stat.size == 0 and !this.is_regular_file) { + // read up to 4k at a time if they didn't explicitly set a size and + // we're reading from something that's not a regular file. + this.size = if (this.max_length == Blob.max_size) + 4096 + else + this.max_length; + } + + if (this.offset > 0) { + // We DO support offset in Bun.file() + switch (bun.sys.setFileOffset(this.opened_fd, this.offset)) { + // we ignore errors because it should continue to work even if its a pipe + .err, .result => {}, } } // Special files might report a size of > 0, and be wrong. // so we should check specifically that its a regular file before trusting the size. - if (this.size == 0 and bun.isRegularFile(this.file_store.mode)) { - this.buffer = &[_]u8{}; - this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); - + if (this.size == 0 and this.is_regular_file) { + this.byte_store = ByteStore.init(this.buffer.items, bun.default_allocator); this.onFinish(); return; } // add an extra 16 bytes to the buffer to avoid having to resize it for trailing extra data - this.buffer = bun.default_allocator.alloc(u8, this.size + 16) catch |err| { + this.buffer.ensureTotalCapacityPrecise(this.byte_store.allocator, this.size + 16) catch |err| { this.errno = err; this.onFinish(); return; @@ -722,23 +717,34 @@ pub const ReadFileUV = struct { this.read_len = 0; this.read_off = 0; + this.req.deinit(); + this.queueRead(); } fn remainingBuffer(this: *const ReadFileUV) []u8 { - var remaining = this.buffer[@min(this.read_off, this.buffer.len)..]; - remaining = remaining[0..@min(remaining.len, this.max_length -| this.read_off)]; - return remaining; + return this.buffer.unusedCapacitySlice(); } pub fn queueRead(this: *ReadFileUV) void { if (this.remainingBuffer().len > 0 and this.errno == null and !this.read_eof) { log("ReadFileUV.queueRead - this.remainingBuffer().len = {d}", .{this.remainingBuffer().len}); + if (!this.is_regular_file) { + // non-regular files have variable sizes, so we always ensure + // theres at least 4096 bytes of free space. there has already + // been an initial allocation done for us + this.buffer.ensureUnusedCapacity(this.byte_store.allocator, 4096) catch |err| { + this.errno = err; + this.onFinish(); + }; + } + const buf = this.remainingBuffer(); var bufs: [1]libuv.uv_buf_t = .{ libuv.uv_buf_t.init(buf), }; + this.req.assertCleanedUp(); const res = libuv.uv_fs_read( this.loop, &this.req, @@ -748,6 +754,7 @@ pub const ReadFileUV = struct { @as(i64, @intCast(this.offset + this.read_off)), &onRead, ); + this.req.data = this; if (res.errEnum()) |errno| { this.errno = bun.errnoToZigErr(errno); this.system_error = bun.sys.Error.fromCode(errno, .read).toSystemError(); @@ -757,9 +764,14 @@ pub const ReadFileUV = struct { log("ReadFileUV.queueRead done", .{}); // We are done reading. - _ = bun.default_allocator.resize(this.buffer, this.read_off); - this.buffer = this.buffer[0..this.read_off]; - this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + this.byte_store = ByteStore.init( + this.buffer.toOwnedSlice(this.byte_store.allocator) catch |err| { + this.errno = err; + this.onFinish(); + return; + }, + bun.default_allocator, + ); this.onFinish(); } } @@ -767,24 +779,33 @@ pub const ReadFileUV = struct { pub fn onRead(req: *libuv.fs_t) callconv(.C) void { var this: *ReadFileUV = @alignCast(@ptrCast(req.data)); - if (req.result.errEnum()) |errno| { + const result = req.result; + + if (result.errEnum()) |errno| { this.errno = bun.errnoToZigErr(errno); this.system_error = bun.sys.Error.fromCode(errno, .read).toSystemError(); this.finalize(); return; } - if (req.result.value == 0) { + if (result.int() == 0) { // We are done reading. - _ = bun.default_allocator.resize(this.buffer, this.read_off); - this.buffer = this.buffer[0..this.read_off]; - this.byte_store = ByteStore.init(this.buffer, bun.default_allocator); + this.byte_store = ByteStore.init( + this.buffer.toOwnedSlice(this.byte_store.allocator) catch |err| { + this.errno = err; + this.onFinish(); + return; + }, + bun.default_allocator, + ); this.onFinish(); return; } - this.read_off += @intCast(req.result.value); + this.read_off += @intCast(result.int()); + this.buffer.items.len += @intCast(result.int()); + this.req.deinit(); this.queueRead(); } }; diff --git a/src/bun.js/webcore/blob/WriteFile.zig b/src/bun.js/webcore/blob/WriteFile.zig index 7b511a9ce5d159..ffc13161d671ef 100644 --- a/src/bun.js/webcore/blob/WriteFile.zig +++ b/src/bun.js/webcore/blob/WriteFile.zig @@ -483,7 +483,7 @@ pub const WriteFileWindows = struct { return; } - this.fd = @intCast(rc.value); + this.fd = @intCast(rc.int()); // the loop must be copied this.doWriteLoop(this.loop()); @@ -535,14 +535,15 @@ pub const WriteFileWindows = struct { return; } - this.total_written += @intCast(rc.value); + this.total_written += @intCast(rc.int()); this.doWriteLoop(this.loop()); } pub fn onFinish(container: *WriteFileWindows) void { container.loop().unrefConcurrently(); var event_loop = container.event_loop; - defer event_loop.drainMicrotasks(); + event_loop.enter(); + defer event_loop.exit(); // We don't need to enqueue task since this is already in a task. container.runFromJSThread(); diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index 800e00d9f91785..a6b28afc0cbcfd 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -87,7 +87,7 @@ pub const Body = struct { try formatter.writeIndent(Writer, writer); try Blob.writeFormatForSize(this.value.size(), writer, enable_ansi_colors); } else if (this.value == .Locked) { - if (this.value.Locked.readable) |stream| { + if (this.value.Locked.readable.get()) |stream| { try formatter.printComma(Writer, writer, enable_ansi_colors); try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); @@ -102,7 +102,7 @@ pub const Body = struct { pub const PendingValue = struct { promise: ?JSValue = null, - readable: ?JSC.WebCore.ReadableStream = null, + readable: JSC.WebCore.ReadableStream.Strong = .{}, // writable: JSC.WebCore.Sink global: *JSGlobalObject, @@ -126,7 +126,7 @@ pub const Body = struct { /// If chunked encoded this will represent the total received size (ignoring the chunk headers) /// If the size is unknown will be 0 fn sizeHint(this: *const PendingValue) Blob.SizeType { - if (this.readable) |readable| { + if (this.readable.get()) |readable| { if (readable.ptr == .Bytes) { return readable.ptr.Bytes.size_hint; } @@ -141,6 +141,26 @@ pub const Body = struct { return this.toAnyBlobAllowPromise(); } + pub fn isDisturbed(this: *const PendingValue, comptime T: type, globalObject: *JSC.JSGlobalObject, this_value: JSC.JSValue) bool { + if (this.promise != null) { + return true; + } + + if (T.bodyGetCached(this_value)) |body_value| { + if (JSC.WebCore.ReadableStream.isDisturbedValue(body_value, globalObject)) { + return true; + } + + return false; + } + + if (this.readable.get()) |readable| { + return readable.isDisturbed(globalObject); + } + + return false; + } + pub fn hasPendingPromise(this: *PendingValue) bool { const promise = this.promise orelse return false; @@ -159,10 +179,10 @@ pub const Body = struct { } pub fn toAnyBlobAllowPromise(this: *PendingValue) ?AnyBlob { - var stream = if (this.readable != null) &this.readable.? else return null; + var stream = if (this.readable.get()) |readable| readable else return null; if (stream.toAnyBlob(this.global)) |blob| { - this.readable = null; + this.readable.deinit(); return blob; } @@ -171,8 +191,7 @@ pub const Body = struct { pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) JSValue { value.action = action; - - if (value.readable) |readable| handle_stream: { + if (value.readable.get()) |readable| handle_stream: { switch (action) { .getFormData, .getText, .getJSON, .getBlob, .getArrayBuffer => { value.promise = switch (action) { @@ -184,13 +203,11 @@ pub const Body = struct { if (value.onStartBuffering != null) { if (readable.isDisturbed(globalThis)) { form_data.?.deinit(); - readable.value.unprotect(); - value.readable = null; + value.readable.deinit(); value.action = .{ .none = {} }; return JSC.JSPromise.rejectedPromiseValue(globalThis, globalThis.createErrorInstance("ReadableStream is already used", .{})); } else { - readable.detachIfPossible(globalThis); - value.readable = null; + value.readable.deinit(); } break :handle_stream; @@ -208,10 +225,9 @@ pub const Body = struct { else => unreachable, }; value.promise.?.ensureStillAlive(); - readable.value.unprotect(); - // js now owns the memory - value.readable = null; + readable.detachIfPossible(globalThis); + value.readable.deinit(); value.promise.?.protect(); return value.promise.?; @@ -407,17 +423,15 @@ pub const Body = struct { this.* = .{ .Locked = .{ - .readable = JSC.WebCore.ReadableStream.fromJS(value, globalThis).?, + .readable = JSC.WebCore.ReadableStream.Strong.init(JSC.WebCore.ReadableStream.fromJS(value, globalThis).?, globalThis), .global = globalThis, }, }; - this.Locked.readable.?.value.protect(); - return value; }, .Locked => { var locked = &this.Locked; - if (locked.readable) |readable| { + if (locked.readable.get()) |readable| { return readable.value; } if (locked.promise != null) { @@ -437,11 +451,10 @@ pub const Body = struct { return JSC.WebCore.ReadableStream.empty(globalThis); } - var reader = bun.default_allocator.create(JSC.WebCore.ByteStream.Source) catch unreachable; - reader.* = .{ + var reader = JSC.WebCore.ByteStream.Source.new(.{ .context = undefined, .globalThis = globalThis, - }; + }); reader.context.setup(); @@ -453,17 +466,16 @@ pub const Body = struct { reader.context.size_hint = @as(Blob.SizeType, @truncate(drain_result.owned.size_hint)); } - locked.readable = .{ + locked.readable = JSC.WebCore.ReadableStream.Strong.init(.{ .ptr = .{ .Bytes = &reader.context }, - .value = reader.toJS(globalThis), - }; - locked.readable.?.value.protect(); + .value = reader.toReadableStream(globalThis), + }, globalThis); if (locked.onReadableStreamAvailable) |onReadableStreamAvailable| { - onReadableStreamAvailable(locked.task.?, locked.readable.?); + onReadableStreamAvailable(locked.task.?, locked.readable.get().?); } - return locked.readable.?.value; + return locked.readable.get().?.value; }, .Error => { // TODO: handle error properly @@ -563,17 +575,15 @@ pub const Body = struct { switch (readable.ptr) { .Blob => |blob| { + const store = blob.detachStore() orelse { + return Body.Value{ .Blob = Blob.initEmpty(globalThis) }; + }; + readable.forceDetach(globalThis); const result: Value = .{ - .Blob = Blob.initWithStore(blob.store, globalThis), + .Blob = Blob.initWithStore(store, globalThis), }; - blob.store.ref(); - - if (!blob.done) { - blob.done = true; - blob.deinit(); - } return result; }, @@ -597,30 +607,22 @@ pub const Body = struct { } pub fn fromReadableStreamWithoutLockCheck(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value { - readable.value.protect(); return .{ .Locked = .{ - .readable = readable, + .readable = JSC.WebCore.ReadableStream.Strong.init(readable, globalThis), .global = globalThis, }, }; } - pub fn fromReadableStream(readable: JSC.WebCore.ReadableStream, globalThis: *JSGlobalObject) Value { - if (readable.isLocked(globalThis)) { - return .{ .Error = ZigString.init("Cannot use a locked ReadableStream").toErrorInstance(globalThis) }; - } - - return fromReadableStreamWithoutLockCheck(readable, globalThis); - } - pub fn resolve(to_resolve: *Value, new: *Value, global: *JSGlobalObject) void { log("resolve", .{}); if (to_resolve.* == .Locked) { var locked = &to_resolve.Locked; - if (locked.readable) |readable| { - readable.done(); - locked.readable = null; + + if (locked.readable.get()) |readable| { + readable.done(global); + locked.readable.deinit(); } if (locked.onReceiveValue) |callback| { @@ -839,9 +841,9 @@ pub const Body = struct { promise.unprotect(); } - if (locked.readable) |readable| { - locked.readable = null; - readable.done(); + if (locked.readable.get()) |readable| { + readable.done(global); + locked.readable.deinit(); } // will be unprotected by body value deinit error_instance.protect(); @@ -873,10 +875,8 @@ pub const Body = struct { if (tag == .Locked) { if (!this.Locked.deinit) { this.Locked.deinit = true; - - if (this.Locked.readable) |*readable| { - readable.done(); - } + this.Locked.readable.deinit(); + this.Locked.readable = .{}; } return; @@ -954,7 +954,7 @@ pub fn BodyMixin(comptime Type: type) type { pub fn getText( this: *Type, globalObject: *JSC.JSGlobalObject, - _: *JSC.CallFrame, + callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { var value: *Body.Value = this.getBodyValue(); if (value.* == .Used) { @@ -962,7 +962,7 @@ pub fn BodyMixin(comptime Type: type) type { } if (value.* == .Locked) { - if (value.Locked.promise != null) { + if (value.Locked.isDisturbed(Type, globalObject, callframe.this())) { return handleBodyAlreadyUsed(globalObject); } @@ -994,7 +994,7 @@ pub fn BodyMixin(comptime Type: type) type { switch (this.getBodyValue().*) { .Used => true, .Locked => |*pending| brk: { - if (pending.readable) |*stream| { + if (pending.readable.get()) |*stream| { break :brk stream.isDisturbed(globalObject); } @@ -1008,7 +1008,7 @@ pub fn BodyMixin(comptime Type: type) type { pub fn getJSON( this: *Type, globalObject: *JSC.JSGlobalObject, - _: *JSC.CallFrame, + callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { var value: *Body.Value = this.getBodyValue(); if (value.* == .Used) { @@ -1016,7 +1016,7 @@ pub fn BodyMixin(comptime Type: type) type { } if (value.* == .Locked) { - if (value.Locked.promise != null) { + if (value.Locked.isDisturbed(Type, globalObject, callframe.this())) { return handleBodyAlreadyUsed(globalObject); } return value.Locked.setPromise(globalObject, .{ .getJSON = {} }); @@ -1038,7 +1038,7 @@ pub fn BodyMixin(comptime Type: type) type { pub fn getArrayBuffer( this: *Type, globalObject: *JSC.JSGlobalObject, - _: *JSC.CallFrame, + callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { var value: *Body.Value = this.getBodyValue(); @@ -1047,7 +1047,7 @@ pub fn BodyMixin(comptime Type: type) type { } if (value.* == .Locked) { - if (value.Locked.promise != null) { + if (value.Locked.isDisturbed(Type, globalObject, callframe.this())) { return handleBodyAlreadyUsed(globalObject); } return value.Locked.setPromise(globalObject, .{ .getArrayBuffer = {} }); @@ -1061,7 +1061,7 @@ pub fn BodyMixin(comptime Type: type) type { pub fn getFormData( this: *Type, globalObject: *JSC.JSGlobalObject, - _: *JSC.CallFrame, + callframe: *JSC.CallFrame, ) callconv(.C) JSC.JSValue { var value: *Body.Value = this.getBodyValue(); @@ -1070,7 +1070,7 @@ pub fn BodyMixin(comptime Type: type) type { } if (value.* == .Locked) { - if (value.Locked.promise != null) { + if (value.Locked.isDisturbed(Type, globalObject, callframe.this())) { return handleBodyAlreadyUsed(globalObject); } } @@ -1178,7 +1178,7 @@ pub const BodyValueBufferer = struct { pub fn deinit(this: *@This()) void { this.stream_buffer.deinit(); if (this.byte_stream) |byte_stream| { - byte_stream.unpipe(); + byte_stream.unpipeWithoutDeref(); } this.readable_stream_ref.deinit(); @@ -1400,10 +1400,9 @@ pub const BodyValueBufferer = struct { fn bufferLockedBodyValue(sink: *@This(), value: *JSC.WebCore.Body.Value) !void { std.debug.assert(value.* == .Locked); const locked = &value.Locked; - if (locked.readable) |stream_| { - const stream: JSC.WebCore.ReadableStream = stream_; - stream.value.ensureStillAlive(); - + if (locked.readable.get()) |stream| { + // keep the stream alive until we're done with it + sink.readable_stream_ref = locked.readable; value.* = .{ .Used = {} }; if (stream.isLocked(sink.global)) { @@ -1432,13 +1431,9 @@ pub const BodyValueBufferer = struct { log("byte stream has_received_last_chunk {}", .{bytes.len}); sink.onFinishedBuffering(sink.ctx, bytes, null, false); // is safe to detach here because we're not going to receive any more data - stream.detachIfPossible(sink.global); + stream.done(sink.global); return; } - // keep the stream alive until we're done with it - sink.readable_stream_ref = try JSC.WebCore.ReadableStream.Strong.init(stream, sink.global); - // we now hold a reference so we can safely ask to detach and will be detached when the last ref is dropped - stream.detachIfPossible(sink.global); byte_stream.pipe = JSC.WebCore.Pipe.New(@This(), onStreamPipe).init(sink); sink.byte_stream = byte_stream; diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 5096cb1474406a..2099bd354cdd34 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -180,7 +180,7 @@ pub const Request = struct { try Blob.writeFormatForSize(size, writer, enable_ansi_colors); } } else if (this.body.value == .Locked) { - if (this.body.value.Locked.readable) |stream| { + if (this.body.value.Locked.readable.get()) |stream| { try writer.writeAll("\n"); try formatter.writeIndent(Writer, writer); formatter.printAs(.Object, Writer, writer, stream.value, stream.value.jsType(), enable_ansi_colors); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index a5c4f36af752df..f1763b64203def 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -901,6 +901,9 @@ pub const Fetch = struct { // clean for reuse later this.scheduled_response_buffer.reset(); } else { + var prev = this.readable_stream_ref; + this.readable_stream_ref = .{}; + defer prev.deinit(); readable.ptr.Bytes.onData( .{ .temporary_and_done = bun.ByteList.initConst(chunk), @@ -914,9 +917,9 @@ pub const Fetch = struct { if (this.response.get()) |response_js| { if (response_js.as(Response)) |response| { - const body = response.body; + var body = &response.body; if (body.value == .Locked) { - if (body.value.Locked.readable) |readable| { + if (body.value.Locked.readable.get()) |readable| { if (readable.ptr == .Bytes) { readable.ptr.Bytes.size_hint = this.getSizeHint(); @@ -935,6 +938,11 @@ pub const Fetch = struct { // clean for reuse later this.scheduled_response_buffer.reset(); } else { + var prev = body.value.Locked.readable; + body.value.Locked.readable = .{}; + readable.value.ensureStillAlive(); + prev.deinit(); + readable.value.ensureStillAlive(); readable.ptr.Bytes.onData( .{ .temporary_and_done = bun.ByteList.initConst(chunk), @@ -1306,7 +1314,7 @@ pub const Fetch = struct { pub fn onReadableStreamAvailable(ctx: *anyopaque, readable: JSC.WebCore.ReadableStream) void { const this = bun.cast(*FetchTasklet, ctx); - this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, this.global_this) catch .{}; + this.readable_stream_ref = JSC.WebCore.ReadableStream.Strong.init(readable, this.global_this); } pub fn onStartStreamingRequestBodyCallback(ctx: *anyopaque) JSC.WebCore.DrainResult { @@ -1944,7 +1952,7 @@ pub const Fetch = struct { method = request.method; if (request.body.value == .Locked) { - if (request.body.value.Locked.readable) |stream| { + if (request.body.value.Locked.readable.get()) |stream| { if (stream.isDisturbed(globalThis)) { globalThis.throw("ReadableStream has already been consumed", .{}); if (hostname) |host| { diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 2b56ac26c86381..dba2fdfff8980a 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -50,7 +50,6 @@ const AnyBlob = JSC.WebCore.AnyBlob; pub const ReadableStream = struct { value: JSValue, ptr: Source, - pub const Strong = struct { held: JSC.Strong = .{}, @@ -58,38 +57,23 @@ pub const ReadableStream = struct { return this.held.globalThis; } - pub fn init(this: ReadableStream, global: *JSGlobalObject) !Strong { - switch (this.ptr) { - .Blob => |stream| { - try stream.parent().incrementCount(); - }, - .File => |stream| { - try stream.parent().incrementCount(); - }, - .Bytes => |stream| { - try stream.parent().incrementCount(); - }, - else => {}, - } + pub fn init(this: ReadableStream, global: *JSGlobalObject) Strong { return .{ .held = JSC.Strong.create(this.value, global), }; } - pub fn get(this: *Strong) ?ReadableStream { - if (this.globalThis()) |global| { - if (this.held.get()) |value| { - return ReadableStream.fromJS(value, global); - } + pub fn get(this: *const Strong) ?ReadableStream { + if (this.held.get()) |value| { + return ReadableStream.fromJS(value, this.held.globalThis.?); } return null; } pub fn deinit(this: *Strong) void { - if (this.get()) |readable| { - // decrement the ref count and if it's zero we auto detach - readable.detachIfPossible(this.globalThis().?); - } + // if (this.held.get()) |val| { + // ReadableStream__detach(val, this.held.globalThis.?); + // } this.held.deinit(); } }; @@ -102,7 +86,6 @@ pub const ReadableStream = struct { if (ReadableStream.fromJS(this.value, globalThis)) |stream| { this.* = stream; } else { - this.value.unprotect(); this.* = .{ .ptr = .{ .Invalid = {} }, .value = .zero }; } } @@ -119,21 +102,21 @@ pub const ReadableStream = struct { switch (stream.ptr) { .Blob => |blobby| { - var blob = JSC.WebCore.Blob.initWithStore(blobby.store, globalThis); + var blob = JSC.WebCore.Blob.initWithStore(blobby.store orelse return null, globalThis); blob.offset = blobby.offset; blob.size = blobby.remain; blob.store.?.ref(); - stream.detachIfPossible(globalThis); + stream.done(globalThis); return AnyBlob{ .Blob = blob }; }, .File => |blobby| { - if (blobby.lazy_readable == .blob) { - var blob = JSC.WebCore.Blob.initWithStore(blobby.lazy_readable.blob, globalThis); + if (blobby.lazy == .blob) { + var blob = JSC.WebCore.Blob.initWithStore(blobby.lazy.blob, globalThis); blob.store.?.ref(); // it should be lazy, file shouldn't have opened yet. std.debug.assert(!blobby.started); - stream.detachIfPossible(globalThis); + stream.done(globalThis); return AnyBlob{ .Blob = blob }; } }, @@ -146,7 +129,7 @@ pub const ReadableStream = struct { blob.from(bytes.buffer); bytes.buffer.items = &.{}; bytes.buffer.capacity = 0; - stream.detachIfPossible(globalThis); + stream.done(globalThis); return blob; } @@ -158,44 +141,33 @@ pub const ReadableStream = struct { return null; } - pub fn done(this: *const ReadableStream) void { - this.value.unprotect(); + pub fn done(this: *const ReadableStream, globalThis: *JSGlobalObject) void { + this.detachIfPossible(globalThis); } pub fn cancel(this: *const ReadableStream, globalThis: *JSGlobalObject) void { JSC.markBinding(@src()); + ReadableStream__cancel(this.value, globalThis); - this.value.unprotect(); + this.detachIfPossible(globalThis); } pub fn abort(this: *const ReadableStream, globalThis: *JSGlobalObject) void { JSC.markBinding(@src()); + ReadableStream__cancel(this.value, globalThis); - this.value.unprotect(); + this.detachIfPossible(globalThis); } pub fn forceDetach(this: *const ReadableStream, globalObject: *JSGlobalObject) void { ReadableStream__detach(this.value, globalObject); - this.value.unprotect(); } /// Decrement Source ref count and detach the underlying stream if ref count is zero /// be careful, this can invalidate the stream do not call this multiple times /// this is meant to be called only once when we are done consuming the stream or from the ReadableStream.Strong.deinit - pub fn detachIfPossible(this: *const ReadableStream, globalThis: *JSGlobalObject) void { + pub fn detachIfPossible(_: *const ReadableStream, _: *JSGlobalObject) void { JSC.markBinding(@src()); - - const ref_count = switch (this.ptr) { - .Blob => |blob| blob.parent().decrementCount(), - .File => |file| file.parent().decrementCount(), - .Bytes => |bytes| bytes.parent().decrementCount(), - else => 0, - }; - - if (ref_count == 0) { - ReadableStream__detach(this.value, globalThis); - this.value.unprotect(); - } } pub const Tag = enum(i32) { @@ -241,7 +213,7 @@ pub const ReadableStream = struct { Bytes: *ByteStream, }; - extern fn ReadableStreamTag__tagged(globalObject: *JSGlobalObject, possibleReadableStream: *JSValue, ptr: *JSValue) Tag; + extern fn ReadableStreamTag__tagged(globalObject: *JSGlobalObject, possibleReadableStream: *JSValue, ptr: *?*anyopaque) Tag; extern fn ReadableStream__isDisturbed(possibleReadableStream: JSValue, globalObject: *JSGlobalObject) bool; extern fn ReadableStream__isLocked(possibleReadableStream: JSValue, globalObject: *JSGlobalObject) bool; extern fn ReadableStream__empty(*JSGlobalObject) JSC.JSValue; @@ -258,7 +230,12 @@ pub const ReadableStream = struct { pub fn isDisturbed(this: *const ReadableStream, globalObject: *JSGlobalObject) bool { JSC.markBinding(@src()); - return ReadableStream__isDisturbed(this.value, globalObject); + return isDisturbedValue(this.value, globalObject); + } + + pub fn isDisturbedValue(value: JSC.JSValue, globalObject: *JSGlobalObject) bool { + JSC.markBinding(@src()); + return ReadableStream__isDisturbed(value, globalObject); } pub fn isLocked(this: *const ReadableStream, globalObject: *JSGlobalObject) bool { @@ -268,8 +245,10 @@ pub const ReadableStream = struct { pub fn fromJS(value: JSValue, globalThis: *JSGlobalObject) ?ReadableStream { JSC.markBinding(@src()); - var ptr = JSValue.zero; + value.ensureStillAlive(); var out = value; + + var ptr: ?*anyopaque = null; return switch (ReadableStreamTag__tagged(globalThis, &out, &ptr)) { .JavaScript => ReadableStream{ .value = out, @@ -280,20 +259,20 @@ pub const ReadableStream = struct { .Blob => ReadableStream{ .value = out, .ptr = .{ - .Blob = ptr.asPtr(ByteBlobLoader), + .Blob = @ptrCast(@alignCast(ptr.?)), }, }, .File => ReadableStream{ .value = out, .ptr = .{ - .File = ptr.asPtr(FileReader), + .File = @ptrCast(@alignCast(ptr.?)), }, }, .Bytes => ReadableStream{ .value = out, .ptr = .{ - .Bytes = ptr.asPtr(ByteStream), + .Bytes = @ptrCast(@alignCast(ptr.?)), }, }, @@ -313,11 +292,11 @@ pub const ReadableStream = struct { }; } - extern fn ZigGlobalObject__createNativeReadableStream(*JSGlobalObject, nativePtr: JSValue, nativeType: JSValue) JSValue; + extern fn ZigGlobalObject__createNativeReadableStream(*JSGlobalObject, nativePtr: JSValue) JSValue; - pub fn fromNative(globalThis: *JSGlobalObject, id: Tag, ptr: *anyopaque) JSC.JSValue { + pub fn fromNative(globalThis: *JSGlobalObject, native: JSC.JSValue) JSC.JSValue { JSC.markBinding(@src()); - return ZigGlobalObject__createNativeReadableStream(globalThis, JSValue.fromPtr(ptr), JSValue.jsNumber(@intFromEnum(id))); + return ZigGlobalObject__createNativeReadableStream(globalThis, native); } pub fn fromBlob(globalThis: *JSGlobalObject, blob: *const Blob, recommended_chunk_size: Blob.SizeType) JSC.JSValue { @@ -327,60 +306,48 @@ pub const ReadableStream = struct { }; switch (store.data) { .bytes => { - var reader = globalThis.allocator().create(ByteBlobLoader.Source) catch unreachable; - reader.* = .{ - .globalThis = globalThis, - .context = undefined, - }; + var reader = ByteBlobLoader.Source.new( + .{ + .globalThis = globalThis, + .context = undefined, + }, + ); reader.context.setup(blob, recommended_chunk_size); - return reader.toJS(globalThis); + return reader.toReadableStream(globalThis); }, .file => { - var reader = globalThis.allocator().create(FileReader.Source) catch unreachable; - reader.* = .{ + var reader = FileReader.Source.new(.{ .globalThis = globalThis, .context = .{ - .lazy_readable = .{ + .event_loop = JSC.EventLoopHandle.init(globalThis.bunVM().eventLoop()), + .lazy = .{ .blob = store, }, }, - }; + }); store.ref(); - return reader.toJS(globalThis); + + return reader.toReadableStream(globalThis); }, } } - pub fn fromFIFO( + pub fn fromPipe( globalThis: *JSGlobalObject, - fifo: *FIFO, - buffered_data: bun.ByteList, + parent: anytype, + buffered_reader: anytype, ) JSC.JSValue { + _ = parent; // autofix JSC.markBinding(@src()); - var reader = globalThis.allocator().create(FileReader.Source) catch unreachable; - reader.* = .{ + var source = FileReader.Source.new(.{ .globalThis = globalThis, .context = .{ - .buffered_data = buffered_data, - .started = true, - .lazy_readable = .{ - .readable = .{ - .FIFO = fifo.*, - }, - }, + .event_loop = JSC.EventLoopHandle.init(globalThis.bunVM().eventLoop()), }, - }; - - if (reader.context.lazy_readable.readable.FIFO.poll_ref) |poll| { - poll.owner.set(&reader.context.lazy_readable.readable.FIFO); - fifo.poll_ref = null; - } - reader.context.lazy_readable.readable.FIFO.pending.future = undefined; - reader.context.lazy_readable.readable.FIFO.auto_sizer = null; - reader.context.lazy_readable.readable.FIFO.pending.state = .none; - reader.context.lazy_readable.readable.FIFO.drained = buffered_data.len == 0; + }); + source.context.reader.from(buffered_reader, &source.context); - return reader.toJS(globalThis); + return source.toReadableStream(globalThis); } pub fn empty(globalThis: *JSGlobalObject) JSC.JSValue { @@ -429,17 +396,26 @@ pub const StreamStart = union(Tag) { as_uint8array: bool, stream: bool, }, - FileSink: struct { - chunk_size: Blob.SizeType = 16384, + FileSink: FileSinkOptions, + HTTPSResponseSink: void, + HTTPResponseSink: void, + ready: void, + owned_and_done: bun.ByteList, + done: bun.ByteList, + + pub const FileSinkOptions = struct { + chunk_size: Blob.SizeType = 1024, input_path: PathOrFileDescriptor, truncate: bool = true, close: bool = false, mode: bun.Mode = 0o664, - }, - HTTPSResponseSink: void, - HTTPResponseSink: void, - UVStreamSink: void, - ready: void, + + pub fn flags(this: *const FileSinkOptions) bun.Mode { + _ = this; + + return std.os.O.NONBLOCK | std.os.O.CLOEXEC | std.os.O.CREAT | std.os.O.WRONLY; + } + }; pub const Tag = enum { empty, @@ -449,8 +425,9 @@ pub const StreamStart = union(Tag) { FileSink, HTTPSResponseSink, HTTPResponseSink, - UVStreamSink, ready, + owned_and_done, + done, }; pub fn toJS(this: StreamStart, globalThis: *JSGlobalObject) JSC.JSValue { @@ -465,6 +442,12 @@ pub const StreamStart = union(Tag) { globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); return JSC.JSValue.jsUndefined(); }, + .owned_and_done => |list| { + return JSC.ArrayBuffer.fromBytes(list.slice(), .Uint8Array).toJS(globalThis, null); + }, + .done => |list| { + return JSC.ArrayBuffer.create(globalThis, list.slice(), .Uint8Array); + }, else => { return JSC.JSValue.jsUndefined(); }, @@ -507,14 +490,14 @@ pub const StreamStart = union(Tag) { } } - if (value.get(globalThis, "stream")) |val| { + if (value.fastGet(globalThis, .stream)) |val| { if (val.isBoolean()) { stream = val.toBoolean(); empty = false; } } - if (value.get(globalThis, "highWaterMark")) |chunkSize| { + if (value.fastGet(globalThis, .highWaterMark)) |chunkSize| { if (chunkSize.isNumber()) { empty = false; chunk_size = @as(JSC.WebCore.Blob.SizeType, @intCast(@max(0, @as(i51, @truncate(chunkSize.toInt64()))))); @@ -534,12 +517,12 @@ pub const StreamStart = union(Tag) { .FileSink => { var chunk_size: JSC.WebCore.Blob.SizeType = 0; - if (value.getTruthy(globalThis, "highWaterMark")) |chunkSize| { + if (value.fastGet(globalThis, .highWaterMark)) |chunkSize| { if (chunkSize.isNumber()) chunk_size = @as(JSC.WebCore.Blob.SizeType, @intCast(@max(0, @as(i51, @truncate(chunkSize.toInt64()))))); } - if (value.getTruthy(globalThis, "path")) |path| { + if (value.fastGet(globalThis, .path)) |path| { if (!path.isString()) { return .{ .err = Syscall.Error{ @@ -593,11 +576,11 @@ pub const StreamStart = union(Tag) { }, }; }, - .UVStreamSink, .HTTPSResponseSink, .HTTPResponseSink => { + .HTTPSResponseSink, .HTTPResponseSink => { var empty = true; var chunk_size: JSC.WebCore.Blob.SizeType = 2048; - if (value.getTruthy(globalThis, "highWaterMark")) |chunkSize| { + if (value.fastGet(globalThis, .highWaterMark)) |chunkSize| { if (chunkSize.isNumber()) { empty = false; chunk_size = @as(JSC.WebCore.Blob.SizeType, @intCast(@max(256, @as(i51, @truncate(chunkSize.toInt64()))))); @@ -655,6 +638,11 @@ pub const StreamResult = union(Tag) { into_array_and_done, }; + pub fn slice16(this: *const StreamResult) []const u16 { + const bytes = this.slice(); + return @as([*]const u16, @ptrCast(@alignCast(bytes.ptr)))[0..std.mem.bytesAsSlice(u16, bytes).len]; + } + pub fn slice(this: *const StreamResult) []const u8 { return switch (this.*) { .owned => |owned| owned.slice(), @@ -684,6 +672,10 @@ pub const StreamResult = union(Tag) { consumed: Blob.SizeType = 0, state: StreamResult.Pending.State = .none, + pub fn deinit(_: *@This()) void { + // TODO: + } + pub const Future = union(enum) { promise: struct { promise: *JSPromise, @@ -744,7 +736,7 @@ pub const StreamResult = union(Tag) { promise: *JSPromise, globalThis: *JSGlobalObject, ) void { - promise.asValue(globalThis).unprotect(); + defer promise.asValue(globalThis).unprotect(); switch (result) { .err => |err| { promise.reject(globalThis, err.toJSC(globalThis)); @@ -852,7 +844,7 @@ pub const StreamResult = union(Tag) { this.state = .used; switch (this.future) { .promise => |p| { - StreamResult.fulfillPromise(this.result, p.promise, p.globalThis); + StreamResult.fulfillPromise(&this.result, p.promise, p.globalThis); }, .handler => |h| { h.handler(h.ctx, this.result); @@ -868,24 +860,35 @@ pub const StreamResult = union(Tag) { }; } - pub fn fulfillPromise(result: StreamResult, promise: *JSC.JSPromise, globalThis: *JSC.JSGlobalObject) void { - promise.asValue(globalThis).unprotect(); - switch (result) { + pub fn fulfillPromise(result: *StreamResult, promise: *JSC.JSPromise, globalThis: *JSC.JSGlobalObject) void { + const loop = globalThis.bunVM().eventLoop(); + const promise_value = promise.asValue(globalThis); + defer promise_value.unprotect(); + + loop.enter(); + defer loop.exit(); + + switch (result.*) { .err => |err| { - if (err == .Error) { - promise.reject(globalThis, err.Error.toJSC(globalThis)); - } else { + const value = brk: { + if (err == .Error) break :brk err.Error.toJSC(globalThis); + const js_err = err.JSValue; js_err.ensureStillAlive(); js_err.unprotect(); - promise.reject(globalThis, js_err); - } + + break :brk js_err; + }; + result.* = .{ .temporary = .{} }; + promise.reject(globalThis, value); }, .done => { promise.resolve(globalThis, JSValue.jsBoolean(false)); }, else => { - promise.resolve(globalThis, result.toJS(globalThis)); + const value = result.toJS(globalThis); + result.* = .{ .temporary = .{} }; + promise.resolve(globalThis, value); }, } } @@ -1253,1483 +1256,645 @@ pub const Sink = struct { } }; -pub const FileSink = NewFileSink(.js); -pub const FileSinkMini = NewFileSink(.mini); -pub fn NewFileSink(comptime EventLoop: JSC.EventLoopKind) type { - return struct { - buffer: bun.ByteList, - allocator: std.mem.Allocator, - done: bool = false, - signal: Signal = .{}, - next: ?Sink = null, - auto_close: bool = false, - auto_truncate: bool = false, - fd: bun.FileDescriptor = bun.invalid_fd, - mode: bun.Mode = 0, - chunk_size: usize = 0, - pending: StreamResult.Writable.Pending = StreamResult.Writable.Pending{ - .result = .{ .done = {} }, - }, - - scheduled_count: u32 = 0, - written: usize = 0, - head: usize = 0, - requested_end: bool = false, - has_adjusted_pipe_size_on_linux: bool = false, - max_write_size: usize = std.math.maxInt(usize), - reachable_from_js: bool = true, - poll_ref: ?*Async.FilePoll = null, - - pub usingnamespace NewReadyWatcher(@This(), .writable, ready); - const log = Output.scoped(.FileSink, false); +pub const ArrayBufferSink = struct { + bytes: bun.ByteList, + allocator: std.mem.Allocator, + done: bool = false, + signal: Signal = .{}, + next: ?Sink = null, + streaming: bool = false, + as_uint8array: bool = false, - const ThisFileSink = @This(); + pub fn connect(this: *ArrayBufferSink, signal: Signal) void { + std.debug.assert(this.reader == null); + this.signal = signal; + } - pub const event_loop_kind = EventLoop; + pub fn start(this: *ArrayBufferSink, stream_start: StreamStart) JSC.Maybe(void) { + this.bytes.len = 0; + var list = this.bytes.listManaged(this.allocator); + list.clearRetainingCapacity(); - pub fn isReachable(this: *const ThisFileSink) bool { - return this.reachable_from_js or !this.signal.isDead(); - } + switch (stream_start) { + .ArrayBufferSink => |config| { + if (config.chunk_size > 0) { + list.ensureTotalCapacityPrecise(config.chunk_size) catch return .{ .err = Syscall.Error.oom }; + this.bytes.update(list); + } - pub fn updateRef(this: *ThisFileSink, value: bool) void { - // if (this.poll_ref) |poll| { - // if (value) - // poll.ref(JSC.VirtualMachine.get()) - // else - // poll.unref(JSC.VirtualMachine.get()); - // } - if (this.poll_ref) |poll| { - if (value) - poll.ref(switch (comptime EventLoop) { - .js => JSC.VirtualMachine.get(), - .mini => JSC.MiniEventLoop.global, - }) - else - poll.unref(switch (comptime EventLoop) { - .js => JSC.VirtualMachine.get(), - .mini => JSC.MiniEventLoop.global, - }); - } + this.as_uint8array = config.as_uint8array; + this.streaming = config.stream; + }, + else => {}, } - const max_fifo_size = 64 * 1024; - pub fn prepare(this: *ThisFileSink, input_path: PathOrFileDescriptor, mode: bun.Mode) JSC.Maybe(void) { - var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - const auto_close = this.auto_close; - const fd = if (!auto_close) - input_path.fd - else switch (bun.sys.open(input_path.path.toSliceZ(&file_buf), std.os.O.WRONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC | std.os.O.CREAT, mode)) { - .result => |_fd| _fd, - .err => |err| return .{ .err = err.withPath(input_path.path.slice()) }, - }; - - if (this.poll_ref == null) { - const stat: bun.Stat = switch (bun.sys.fstat(fd)) { - .result => |result| result, - .err => |err| { - if (auto_close) { - _ = bun.sys.close(fd); - } - return .{ .err = err.withPathLike(input_path) }; - }, - }; + this.done = false; - this.mode = @intCast(stat.mode); - this.auto_truncate = this.auto_truncate and (bun.isRegularFile(this.mode)); - } else { - this.auto_truncate = false; - this.max_write_size = max_fifo_size; - } + this.signal.start(); + return .{ .result = {} }; + } - this.fd = fd; + pub fn flush(_: *ArrayBufferSink) JSC.Maybe(void) { + return .{ .result = {} }; + } - return .{ .result = {} }; + pub fn flushFromJS(this: *ArrayBufferSink, globalThis: *JSGlobalObject, wait: bool) JSC.Maybe(JSValue) { + if (this.streaming) { + const value: JSValue = switch (this.as_uint8array) { + true => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .Uint8Array), + false => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .ArrayBuffer), + }; + this.bytes.len = 0; + if (wait) {} + return .{ .result = value }; } - pub fn connect(this: *ThisFileSink, signal: Signal) void { - std.debug.assert(this.reader == null); - this.signal = signal; + return .{ .result = JSValue.jsNumber(0) }; + } + + pub fn finalize(this: *ArrayBufferSink) void { + if (this.bytes.len > 0) { + this.bytes.listManaged(this.allocator).deinit(); + this.bytes = bun.ByteList.init(""); + this.done = true; } - pub fn start(this: *ThisFileSink, stream_start: StreamStart) JSC.Maybe(void) { - this.done = false; - this.written = 0; - this.auto_close = false; - this.auto_truncate = false; - this.requested_end = false; + this.allocator.destroy(this); + } - this.buffer.len = 0; + pub fn init(allocator: std.mem.Allocator, next: ?Sink) !*ArrayBufferSink { + const this = try allocator.create(ArrayBufferSink); + this.* = ArrayBufferSink{ + .bytes = bun.ByteList.init(&.{}), + .allocator = allocator, + .next = next, + }; + return this; + } - switch (stream_start) { - .FileSink => |config| { - this.chunk_size = config.chunk_size; - this.auto_close = config.close or config.input_path == .path; - this.auto_truncate = config.truncate; + pub fn construct( + this: *ArrayBufferSink, + allocator: std.mem.Allocator, + ) void { + this.* = ArrayBufferSink{ + .bytes = bun.ByteList{}, + .allocator = allocator, + .next = null, + }; + } - switch (this.prepare(config.input_path, config.mode)) { - .err => |err| { - return .{ .err = err }; - }, - .result => {}, - } - }, - else => {}, - } + pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.next) |*next| { + return next.writeBytes(data); + } - this.signal.start(); - return .{ .result = {} }; + const len = this.bytes.write(this.allocator, data.slice()) catch { + return .{ .err = Syscall.Error.oom }; + }; + this.signal.ready(null, null); + return .{ .owned = len }; + } + pub const writeBytes = write; + pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.next) |*next| { + return next.writeLatin1(data); + } + const len = this.bytes.writeLatin1(this.allocator, data.slice()) catch { + return .{ .err = Syscall.Error.oom }; + }; + this.signal.ready(null, null); + return .{ .owned = len }; + } + pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.next) |*next| { + return next.writeUTF16(data); } + const len = this.bytes.writeUTF16(this.allocator, @as([*]const u16, @ptrCast(@alignCast(data.slice().ptr)))[0..std.mem.bytesAsSlice(u16, data.slice()).len]) catch { + return .{ .err = Syscall.Error.oom }; + }; + this.signal.ready(null, null); + return .{ .owned = len }; + } - pub fn flush(this: *ThisFileSink, buf: []const u8) StreamResult.Writable { - return this.flushMaybePollWithSizeAndBuffer(buf, std.math.maxInt(usize)); + pub fn end(this: *ArrayBufferSink, err: ?Syscall.Error) JSC.Maybe(void) { + if (this.next) |*next| { + return next.end(err); + } + this.signal.close(err); + return .{ .result = {} }; + } + pub fn destroy(this: *ArrayBufferSink) void { + this.bytes.deinitWithAllocator(this.allocator); + this.allocator.destroy(this); + } + pub fn toJS(this: *ArrayBufferSink, globalThis: *JSGlobalObject, as_uint8array: bool) JSValue { + if (this.streaming) { + const value: JSValue = switch (as_uint8array) { + true => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .Uint8Array), + false => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .ArrayBuffer), + }; + this.bytes.len = 0; + return value; } - fn adjustPipeLengthOnLinux(this: *ThisFileSink, fd: bun.FileDescriptor, remain_len: usize) void { - // On Linux, we can adjust the pipe size to avoid blocking. - this.has_adjusted_pipe_size_on_linux = true; + var list = this.bytes.listManaged(this.allocator); + this.bytes = bun.ByteList.init(""); + return ArrayBuffer.fromBytes( + try list.toOwnedSlice(), + if (as_uint8array) + .Uint8Array + else + .ArrayBuffer, + ).toJS(globalThis, null); + } - switch (bun.sys.setPipeCapacityOnLinux(fd, @min(Syscall.getMaxPipeSizeOnLinux(), remain_len))) { - .result => |len| { - if (len > 0) { - this.max_write_size = len; - } - }, - else => {}, - } + pub fn endFromJS(this: *ArrayBufferSink, _: *JSGlobalObject) JSC.Maybe(ArrayBuffer) { + if (this.done) { + return .{ .result = ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer) }; } - pub fn flushMaybePollWithSizeAndBuffer(this: *ThisFileSink, buffer: []const u8, writable_size: usize) StreamResult.Writable { - std.debug.assert(this.fd != bun.invalid_fd); + std.debug.assert(this.next == null); + var list = this.bytes.listManaged(this.allocator); + this.bytes = bun.ByteList.init(""); + this.done = true; + this.signal.close(null); + return .{ .result = ArrayBuffer.fromBytes( + list.toOwnedSlice() catch @panic("TODO"), + if (this.as_uint8array) + .Uint8Array + else + .ArrayBuffer, + ) }; + } - var total = this.written; - const initial = total; - const fd = this.fd; - var remain = buffer; - remain = remain[@min(this.head, remain.len)..]; - if (remain.len == 0) return .{ .owned = 0 }; + pub fn sink(this: *ArrayBufferSink) Sink { + return Sink.init(this); + } - defer this.written = total; + pub const JSSink = NewJSSink(@This(), "ArrayBufferSink"); +}; - const initial_remain = remain; - defer { - std.debug.assert(total - initial == @intFromPtr(remain.ptr) - @intFromPtr(initial_remain.ptr)); - - if (remain.len == 0) { - this.head = 0; - this.buffer.len = 0; - } else { - this.head += total - initial; - } - } - const is_fifo = this.isFIFO(); - var did_adjust_pipe_size_on_linux_this_tick = false; - if (comptime Environment.isLinux) { - if (is_fifo and !this.has_adjusted_pipe_size_on_linux and remain.len >= (max_fifo_size - 1024)) { - this.adjustPipeLengthOnLinux(fd, remain.len); - did_adjust_pipe_size_on_linux_this_tick = true; - } - } +const AutoFlusher = struct { + registered: bool = false, - const max_to_write = - if (is_fifo) - brk: { - if (comptime Environment.isLinux) { - if (did_adjust_pipe_size_on_linux_this_tick) - break :brk this.max_write_size; - } + pub fn registerDeferredMicrotaskWithType(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { + if (this.auto_flusher.registered) return; + registerDeferredMicrotaskWithTypeUnchecked(Type, this, vm); + } - // The caller may have informed us of the size - // in which case we should use that. - if (writable_size != std.math.maxInt(usize)) - break :brk writable_size; + pub fn unregisterDeferredMicrotaskWithType(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { + if (!this.auto_flusher.registered) return; + unregisterDeferredMicrotaskWithTypeUnchecked(Type, this, vm); + } - if (this.poll_ref) |poll| { - if (poll.isHUP()) { - this.done = true; - this.cleanup(); - return .{ .done = {} }; - } + pub fn unregisterDeferredMicrotaskWithTypeUnchecked(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { + std.debug.assert(this.auto_flusher.registered); + std.debug.assert(vm.eventLoop().deferred_tasks.unregisterTask(this)); + this.auto_flusher.registered = false; + } - if (poll.isWritable()) { - break :brk this.max_write_size; - } - } + pub fn registerDeferredMicrotaskWithTypeUnchecked(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { + std.debug.assert(!this.auto_flusher.registered); + this.auto_flusher.registered = true; + std.debug.assert(!vm.eventLoop().deferred_tasks.postTask(this, @ptrCast(&Type.onAutoFlush))); + } +}; - switch (bun.isWritable(fd)) { - .not_ready => { - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - } +pub const SinkDestructor = struct { + const Detached = opaque {}; + const Subprocess = JSC.API.Bun.Subprocess; + pub const Ptr = bun.TaggedPointerUnion(.{ + Detached, + Subprocess, + }); + + pub export fn Bun__onSinkDestroyed( + ptr_value: ?*anyopaque, + sink_ptr: ?*anyopaque, + ) callconv(.C) void { + _ = sink_ptr; // autofix + const ptr = Ptr.from(ptr_value); + + if (ptr.isNull()) { + return; + } - if (!this.isWatching()) - this.watch(fd); + switch (ptr.tag()) { + .Detached => { + return; + }, + .Subprocess => { + const subprocess = ptr.as(Subprocess); + subprocess.onStdinDestroyed(); + }, + else => { + Output.debugWarn("Unknown sink type", .{}); + }, + } + } +}; - return .{ - .pending = &this.pending, - }; - }, - .hup => { - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - poll.flags.insert(.hup); - } +pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { + return struct { + sink: SinkType, - this.cleanup(); + const ThisSink = @This(); - return .{ - .done = {}, - }; - }, - .ready => break :brk this.max_write_size, - } - } else remain.len; - - if (max_to_write > 0) { - while (remain.len > 0) { - const write_buf = remain[0..@min(remain.len, max_to_write)]; - const res = bun.sys.write(fd, write_buf); - // this does not fix the issue with writes not showing up - // const res = bun.sys.sys_uv.write(fd, write_buf); - - if (res == .err) { - const retry = - E.AGAIN; - - switch (res.err.getErrno()) { - retry => { - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - } - - if (!this.isWatching()) - this.watch(fd); - return .{ - .pending = &this.pending, - }; - }, - .PIPE => { - this.cleanup(); - this.pending.consumed = @as(Blob.SizeType, @truncate(total - initial)); - return .{ .done = {} }; - }, - else => {}, - } - this.pending.result = .{ .err = res.err }; - this.pending.consumed = @as(Blob.SizeType, @truncate(total - initial)); + pub const shim = JSC.Shimmer("", name_, @This()); + pub const name = std.fmt.comptimePrint("{s}", .{name_}); - return .{ .err = res.err }; - } + // This attaches it to JS + pub const SinkSignal = extern struct { + cpp: JSValue, - remain = remain[res.result..]; - total += res.result; + pub fn init(cpp: JSValue) Signal { + // this one can be null + @setRuntimeSafety(false); + return Signal.initWithType(SinkSignal, @as(*SinkSignal, @ptrFromInt(@as(usize, @bitCast(@intFromEnum(cpp)))))); + } - log("Wrote {d} bytes (fd: {}, head: {d}, {d}/{d})", .{ res.result, fd, this.head, remain.len, total }); + pub fn close(this: *@This(), _: ?Syscall.Error) void { + onClose(@as(SinkSignal, @bitCast(@intFromPtr(this))).cpp, JSValue.jsUndefined()); + } - if (res.result == 0) { - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - } - break; - } + pub fn ready(this: *@This(), _: ?Blob.SizeType, _: ?Blob.SizeType) void { + onReady(@as(SinkSignal, @bitCast(@intFromPtr(this))).cpp, JSValue.jsUndefined(), JSValue.jsUndefined()); + } - // we flushed an entire fifo - // but we still have more - // lets check if its writable, so we avoid blocking - if (is_fifo and remain.len > 0) { - switch (bun.isWritable(fd)) { - .ready => { - if (this.poll_ref) |poll_ref| { - poll_ref.flags.insert(.writable); - poll_ref.flags.insert(.fifo); - std.debug.assert(poll_ref.flags.contains(.poll_writable)); - } - }, - .not_ready => { - if (!this.isWatching()) - this.watch(this.fd); - - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - std.debug.assert(poll.flags.contains(.poll_writable)); - } - this.pending.consumed = @as(Blob.SizeType, @truncate(total - initial)); - - return .{ - .pending = &this.pending, - }; - }, - .hup => { - if (this.poll_ref) |poll| { - poll.flags.remove(.writable); - poll.flags.insert(.hup); - } + pub fn start(_: *@This()) void {} + }; - this.cleanup(); + pub fn onClose(ptr: JSValue, reason: JSValue) callconv(.C) void { + JSC.markBinding(@src()); - return .{ - .done = {}, - }; - }, - } - } - } - } + return shim.cppFn("onClose", .{ ptr, reason }); + } - this.pending.result = .{ - .owned = @as(Blob.SizeType, @truncate(total)), - }; - this.pending.consumed = @as(Blob.SizeType, @truncate(total - initial)); + pub fn onReady(ptr: JSValue, amount: JSValue, offset: JSValue) callconv(.C) void { + JSC.markBinding(@src()); - if (is_fifo and remain.len == 0 and this.isWatching()) { - this.unwatch(fd); - } + return shim.cppFn("onReady", .{ ptr, amount, offset }); + } - if (this.requested_end) { - this.done = true; + pub fn onStart(ptr: JSValue, globalThis: *JSGlobalObject) callconv(.C) void { + JSC.markBinding(@src()); - if (is_fifo and this.isWatching()) { - this.unwatch(fd); - } + return shim.cppFn("onStart", .{ ptr, globalThis }); + } - if (this.auto_truncate) - _ = bun.sys.ftruncate(fd, @intCast(total)); + pub fn createObject(globalThis: *JSGlobalObject, object: *anyopaque, destructor: usize) callconv(.C) JSValue { + JSC.markBinding(@src()); - if (this.auto_close) { - _ = bun.sys.close(fd); - this.fd = bun.invalid_fd; - } - } - this.pending.run(); - return .{ .owned = @as(Blob.SizeType, @truncate(total - initial)) }; + return shim.cppFn("createObject", .{ globalThis, object, destructor }); } - pub fn flushFromJS(this: *ThisFileSink, globalThis: *JSGlobalObject, _: bool) JSC.Maybe(JSValue) { - if (this.isPending() or this.done) { - return .{ .result = JSC.JSValue.jsUndefined() }; - } - const result = this.flush(this.buffer.slice()); - - if (result == .err) { - return .{ .err = result.err }; - } + pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) ?*anyopaque { + JSC.markBinding(@src()); - return JSC.Maybe(JSValue){ - .result = result.toJS(globalThis), - }; + return shim.cppFn("fromJS", .{ globalThis, value }); } - fn cleanup(this: *ThisFileSink) void { - this.done = true; - - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinitForceUnregister(); - } + pub fn setDestroyCallback(value: JSValue, callback: usize) void { + JSC.markBinding(@src()); - if (this.auto_close) { - if (this.fd != bun.invalid_fd) { - if (this.scheduled_count > 0) { - this.scheduled_count = 0; - } + return shim.cppFn("setDestroyCallback", .{ value, callback }); + } - _ = bun.sys.close(this.fd); - this.fd = bun.invalid_fd; - } - } + pub fn construct(globalThis: *JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); - if (this.buffer.cap > 0) { - this.buffer.listManaged(this.allocator).deinit(); - this.buffer = bun.ByteList.init(""); - this.head = 0; + if (comptime !@hasDecl(SinkType, "construct")) { + const Static = struct { + pub const message = std.fmt.comptimePrint("{s} is not constructable", .{SinkType.name}); + }; + const err = JSC.SystemError{ + .message = bun.String.static(Static.message), + .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), + }; + globalThis.throwValue(err.toErrorInstance(globalThis)); + return JSC.JSValue.jsUndefined(); } - this.pending.result = .done; - this.pending.run(); + var allocator = globalThis.bunVM().allocator; + var this = allocator.create(ThisSink) catch { + globalThis.vm().throwError(globalThis, Syscall.Error.oom.toJSC( + globalThis, + )); + return JSC.JSValue.jsUndefined(); + }; + this.sink.construct(allocator); + return createObject(globalThis, this, 0); } - pub fn finalize(this: *ThisFileSink) void { - this.cleanup(); - this.signal.close(null); - - this.reachable_from_js = false; + pub fn finalize(ptr: *anyopaque) callconv(.C) void { + var this = @as(*ThisSink, @ptrCast(@alignCast(ptr))); - if (!this.isReachable()) - this.allocator.destroy(this); + this.sink.finalize(); } - pub fn init(allocator: std.mem.Allocator, next: ?Sink) !*FileSink { - const this = try allocator.create(FileSink); - this.* = FileSink{ - .buffer = bun.ByteList{}, - .allocator = allocator, - .next = next, - }; - return this; + pub fn detach(this: *ThisSink) void { + if (comptime !@hasField(SinkType, "signal")) + return; + + const ptr = this.sink.signal.ptr; + if (this.sink.signal.isDead()) + return; + this.sink.signal.clear(); + const value = @as(JSValue, @enumFromInt(@as(JSC.JSValueReprInt, @bitCast(@intFromPtr(ptr))))); + value.unprotect(); + detachPtr(value); } - pub fn construct( - this: *ThisFileSink, - allocator: std.mem.Allocator, - ) void { - this.* = FileSink{ - .buffer = bun.ByteList{}, - .allocator = allocator, - .next = null, - }; + pub fn detachPtr(ptr: JSValue) callconv(.C) void { + shim.cppFn("detachPtr", .{ptr}); } - pub fn toJS(this: *ThisFileSink, globalThis: *JSGlobalObject) JSValue { - return JSSink.createObject(globalThis, this); + fn getThis(globalThis: *JSGlobalObject, callframe: *const JSC.CallFrame) ?*ThisSink { + return @as( + *ThisSink, + @ptrCast(@alignCast( + fromJS( + globalThis, + callframe.this(), + ) orelse return null, + )), + ); } - pub fn ready(this: *ThisFileSink, writable: i64) void { - var remain = this.buffer.slice(); - const pending = remain[@min(this.head, remain.len)..].len; - if (pending == 0) { - if (this.isWatching()) { - this.unwatch(this.fd); - } + fn invalidThis(globalThis: *JSGlobalObject) JSValue { + const err = JSC.toTypeError(JSC.Node.ErrorCode.ERR_INVALID_THIS, "Expected Sink", .{}, globalThis); + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); + } - return; - } + pub fn unprotect(this: *@This()) void { + _ = this; // autofix - if (comptime Environment.isMac) { - _ = this.flushMaybePollWithSizeAndBuffer(this.buffer.slice(), @as(usize, @intCast(@max(writable, 0)))); - } else { - _ = this.flushMaybePollWithSizeAndBuffer(this.buffer.slice(), std.math.maxInt(usize)); - } } - pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.done) { - return .{ .done = {} }; - } - const input = data.slice(); + pub fn write(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - if (!this.isPending() and this.buffer.len == 0 and input.len >= this.chunk_size) { - const result = this.flush(input); - if (this.isPending()) { - _ = this.buffer.write(this.allocator, input) catch { - return .{ .err = Syscall.Error.oom }; - }; + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); } - - return result; } - const len = this.buffer.write(this.allocator, input) catch { - return .{ .err = Syscall.Error.oom }; - }; + const args_list = callframe.arguments(4); + const args = args_list.ptr[0..args_list.len]; - if (!this.isPending() and this.buffer.len >= this.chunk_size) { - return this.flush(this.buffer.slice()); + if (args.len == 0) { + globalThis.vm().throwError(globalThis, JSC.toTypeError( + JSC.Node.ErrorCode.ERR_MISSING_ARGS, + "write() expects a string, ArrayBufferView, or ArrayBuffer", + .{}, + globalThis, + )); + return JSC.JSValue.jsUndefined(); } - this.signal.ready(null, null); - return .{ .owned = len }; - } - pub const writeBytes = write; - pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.done) { - return .{ .done = {} }; - } + const arg = args[0]; + arg.ensureStillAlive(); + defer arg.ensureStillAlive(); - const input = data.slice(); + if (arg.isEmptyOrUndefinedOrNull()) { + globalThis.vm().throwError(globalThis, JSC.toTypeError( + JSC.Node.ErrorCode.ERR_STREAM_NULL_VALUES, + "write() expects a string, ArrayBufferView, or ArrayBuffer", + .{}, + globalThis, + )); + return JSC.JSValue.jsUndefined(); + } - if (!this.isPending() and this.buffer.len == 0 and input.len >= this.chunk_size and strings.isAllASCII(input)) { - const result = this.flush(input); - if (this.isPending()) { - _ = this.buffer.write(this.allocator, input) catch { - return .{ .err = Syscall.Error.oom }; - }; + if (arg.asArrayBuffer(globalThis)) |buffer| { + const slice = buffer.slice(); + if (slice.len == 0) { + return JSC.JSValue.jsNumber(0); } - return result; + return this.sink.writeBytes(.{ .temporary = bun.ByteList.init(slice) }).toJS(globalThis); } - const len = this.buffer.writeLatin1(this.allocator, input) catch { - return .{ .err = Syscall.Error.oom }; - }; - - if (!this.isPending() and this.buffer.len >= this.chunk_size) { - return this.flush(this.buffer.slice()); + if (!arg.isString()) { + globalThis.vm().throwError(globalThis, JSC.toTypeError( + JSC.Node.ErrorCode.ERR_INVALID_ARG_TYPE, + "write() expects a string, ArrayBufferView, or ArrayBuffer", + .{}, + globalThis, + )); + return JSC.JSValue.jsUndefined(); } - this.signal.ready(null, null); - return .{ .owned = len }; - } - pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.done) { - return .{ .done = {} }; + const str = arg.getZigString(globalThis); + if (str.len == 0) { + return JSC.JSValue.jsNumber(0); } - if (this.next) |*next| { - return next.writeUTF16(data); + if (str.is16Bit()) { + return this.sink.writeUTF16(.{ .temporary = bun.ByteList.initConst(std.mem.sliceAsBytes(str.utf16SliceAligned())) }).toJS(globalThis); } - const len = this.buffer.writeUTF16(this.allocator, @as([*]const u16, @ptrCast(@alignCast(data.slice().ptr)))[0..std.mem.bytesAsSlice(u16, data.slice()).len]) catch { - return .{ .err = Syscall.Error.oom }; - }; - if (!this.isPending() and this.buffer.len >= this.chunk_size) { - return this.flush(this.buffer.slice()); - } - this.signal.ready(null, null); - - return .{ .owned = len }; + return this.sink.writeLatin1(.{ .temporary = bun.ByteList.initConst(str.slice()) }).toJS(globalThis); } - fn isPending(this: *const ThisFileSink) bool { - if (this.done) return false; - return this.pending.state == .pending; - } + pub fn writeUTF8(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); - pub fn close(this: *ThisFileSink) void { - if (this.done) - return; + var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - this.done = true; - const fd = this.fd; - const signal_close = fd != bun.invalid_fd; - defer if (signal_close) this.signal.close(null); - if (signal_close) { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinitForceUnregister(); + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); } + } - this.fd = bun.invalid_fd; - if (this.auto_close) - _ = bun.sys.close(fd); + const args_list = callframe.arguments(4); + const args = args_list.ptr[0..args_list.len]; + if (args.len == 0 or !args[0].isString()) { + const err = JSC.toTypeError( + if (args.len == 0) JSC.Node.ErrorCode.ERR_MISSING_ARGS else JSC.Node.ErrorCode.ERR_INVALID_ARG_TYPE, + "writeUTF8() expects a string", + .{}, + globalThis, + ); + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); } - this.pending.result = .done; - this.pending.run(); - } + const arg = args[0]; - pub fn end(this: *ThisFileSink, err: ?Syscall.Error) JSC.Maybe(void) { - if (this.done) { - return .{ .result = {} }; + const str = arg.getZigString(globalThis); + if (str.len == 0) { + return JSC.JSValue.jsNumber(0); } - if (this.next) |*next| { - return next.end(err); + if (str.is16Bit()) { + return this.sink.writeUTF16(.{ .temporary = str.utf16SliceAligned() }).toJS(globalThis); } - if (this.requested_end or this.done) - return .{ .result = {} }; - - this.requested_end = true; - - const flushy = this.flush(this.buffer.slice()); + return this.sink.writeLatin1(.{ .temporary = str.slice() }).toJS(globalThis); + } - if (flushy == .err) { - return .{ .err = flushy.err }; - } + pub fn close(globalThis: *JSGlobalObject, sink_ptr: ?*anyopaque) callconv(.C) JSValue { + JSC.markBinding(@src()); + var this = @as(*ThisSink, @ptrCast(@alignCast(sink_ptr orelse return invalidThis(globalThis)))); - if (flushy != .pending) { - this.cleanup(); + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); + } } - this.signal.close(err); - return .{ .result = {} }; + return this.sink.end(null).toJS(globalThis); } - pub fn endFromJS(this: *ThisFileSink, globalThis: *JSGlobalObject) JSC.Maybe(JSValue) { - if (this.done) { - return .{ .result = JSValue.jsNumber(this.written) }; - } + pub fn flush(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); - std.debug.assert(this.next == null); - this.requested_end = true; + var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - if (this.fd == bun.invalid_fd) { - this.cleanup(); - return .{ .result = JSValue.jsNumber(this.written) }; + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); + } } - const flushed = this.flush(this.buffer.slice()); - - if (flushed == .err) { - return .{ .err = flushed.err }; + defer { + if ((comptime @hasField(SinkType, "done")) and this.sink.done) { + this.unprotect(); + } } - if (flushed != .pending) { - this.cleanup(); + if (comptime @hasDecl(SinkType, "flushFromJS")) { + const wait = callframe.argumentsCount() > 0 and + callframe.argument(0).isBoolean() and + callframe.argument(0).asBoolean(); + const maybe_value: JSC.Maybe(JSValue) = this.sink.flushFromJS(globalThis, wait); + return switch (maybe_value) { + .result => |value| value, + .err => |err| blk: { + globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); + break :blk JSC.JSValue.jsUndefined(); + }, + }; } - this.signal.close(null); - - return .{ .result = flushed.toJS(globalThis) }; + return this.sink.flush().toJS(globalThis); } - pub fn sink(this: *ThisFileSink) Sink { - return Sink.init(this); - } + pub fn start(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); - pub const JSSink = NewJSSink(@This(), "FileSink"); - }; -} + var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); -pub const ArrayBufferSink = struct { - bytes: bun.ByteList, - allocator: std.mem.Allocator, - done: bool = false, - signal: Signal = .{}, - next: ?Sink = null, - streaming: bool = false, - as_uint8array: bool = false, + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); + } + } - pub fn connect(this: *ArrayBufferSink, signal: Signal) void { - std.debug.assert(this.reader == null); - this.signal = signal; - } + if (comptime @hasField(StreamStart, name_)) { + return this.sink.start( + if (callframe.argumentsCount() > 0) + StreamStart.fromJSWithTag( + globalThis, + callframe.argument(0), + comptime @field(StreamStart, name_), + ) + else + StreamStart{ .empty = {} }, + ).toJS(globalThis); + } - pub fn start(this: *ArrayBufferSink, stream_start: StreamStart) JSC.Maybe(void) { - this.bytes.len = 0; - var list = this.bytes.listManaged(this.allocator); - list.clearRetainingCapacity(); + return this.sink.start( + if (callframe.argumentsCount() > 0) + StreamStart.fromJS(globalThis, callframe.argument(0)) + else + StreamStart{ .empty = {} }, + ).toJS(globalThis); + } - switch (stream_start) { - .ArrayBufferSink => |config| { - if (config.chunk_size > 0) { - list.ensureTotalCapacityPrecise(config.chunk_size) catch return .{ .err = Syscall.Error.oom }; - this.bytes.update(list); + pub fn end(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + JSC.markBinding(@src()); + + var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); + + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); } + } - this.as_uint8array = config.as_uint8array; - this.streaming = config.stream; - }, - else => {}, + defer { + if (comptime @hasField(SinkType, "done")) { + if (this.sink.done) { + callframe.this().unprotect(); + } + } + } + + return this.sink.endFromJS(globalThis).toJS(globalThis); } - this.done = false; + pub fn endWithSink(ptr: *anyopaque, globalThis: *JSGlobalObject) callconv(.C) JSValue { + JSC.markBinding(@src()); - this.signal.start(); - return .{ .result = {} }; - } + var this = @as(*ThisSink, @ptrCast(@alignCast(ptr))); - pub fn flush(_: *ArrayBufferSink) JSC.Maybe(void) { - return .{ .result = {} }; - } + if (comptime @hasDecl(SinkType, "getPendingError")) { + if (this.sink.getPendingError()) |err| { + globalThis.vm().throwError(globalThis, err); + return JSC.JSValue.jsUndefined(); + } + } - pub fn flushFromJS(this: *ArrayBufferSink, globalThis: *JSGlobalObject, wait: bool) JSC.Maybe(JSValue) { - if (this.streaming) { - const value: JSValue = switch (this.as_uint8array) { - true => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .Uint8Array), - false => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .ArrayBuffer), - }; - this.bytes.len = 0; - if (wait) {} - return .{ .result = value }; + return this.sink.endFromJS(globalThis).toJS(globalThis); } - return .{ .result = JSValue.jsNumber(0) }; - } - - pub fn finalize(this: *ArrayBufferSink) void { - if (this.bytes.len > 0) { - this.bytes.listManaged(this.allocator).deinit(); - this.bytes = bun.ByteList.init(""); - this.done = true; + pub fn assignToStream(globalThis: *JSGlobalObject, stream: JSValue, ptr: *anyopaque, jsvalue_ptr: **anyopaque) JSValue { + return shim.cppFn("assignToStream", .{ globalThis, stream, ptr, jsvalue_ptr }); } - this.allocator.destroy(this); - } - - pub fn init(allocator: std.mem.Allocator, next: ?Sink) !*ArrayBufferSink { - const this = try allocator.create(ArrayBufferSink); - this.* = ArrayBufferSink{ - .bytes = bun.ByteList.init(&.{}), - .allocator = allocator, - .next = next, - }; - return this; - } + pub const Export = shim.exportFunctions(.{ + .finalize = finalize, + .write = write, + .close = close, + .flush = flush, + .start = start, + .end = end, + .construct = construct, + .endWithSink = endWithSink, + .updateRef = updateRef, + }); - pub fn construct( - this: *ArrayBufferSink, - allocator: std.mem.Allocator, - ) void { - this.* = ArrayBufferSink{ - .bytes = bun.ByteList{}, - .allocator = allocator, - .next = null, - }; - } + pub fn updateRef(ptr: *anyopaque, value: bool) callconv(.C) void { + JSC.markBinding(@src()); + var this = bun.cast(*ThisSink, ptr); + if (comptime @hasDecl(SinkType, "updateRef")) + this.sink.updateRef(value); + } - pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeBytes(data); - } - - const len = this.bytes.write(this.allocator, data.slice()) catch { - return .{ .err = Syscall.Error.oom }; - }; - this.signal.ready(null, null); - return .{ .owned = len }; - } - pub const writeBytes = write; - pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeLatin1(data); - } - const len = this.bytes.writeLatin1(this.allocator, data.slice()) catch { - return .{ .err = Syscall.Error.oom }; - }; - this.signal.ready(null, null); - return .{ .owned = len }; - } - pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeUTF16(data); - } - const len = this.bytes.writeUTF16(this.allocator, @as([*]const u16, @ptrCast(@alignCast(data.slice().ptr)))[0..std.mem.bytesAsSlice(u16, data.slice()).len]) catch { - return .{ .err = Syscall.Error.oom }; - }; - this.signal.ready(null, null); - return .{ .owned = len }; - } - - pub fn end(this: *ArrayBufferSink, err: ?Syscall.Error) JSC.Maybe(void) { - if (this.next) |*next| { - return next.end(err); - } - this.signal.close(err); - return .{ .result = {} }; - } - pub fn destroy(this: *ArrayBufferSink) void { - this.bytes.deinitWithAllocator(this.allocator); - this.allocator.destroy(this); - } - pub fn toJS(this: *ArrayBufferSink, globalThis: *JSGlobalObject, as_uint8array: bool) JSValue { - if (this.streaming) { - const value: JSValue = switch (as_uint8array) { - true => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .Uint8Array), - false => JSC.ArrayBuffer.create(globalThis, this.bytes.slice(), .ArrayBuffer), - }; - this.bytes.len = 0; - return value; - } - - var list = this.bytes.listManaged(this.allocator); - this.bytes = bun.ByteList.init(""); - return ArrayBuffer.fromBytes( - try list.toOwnedSlice(), - if (as_uint8array) - .Uint8Array - else - .ArrayBuffer, - ).toJS(globalThis, null); - } - - pub fn endFromJS(this: *ArrayBufferSink, _: *JSGlobalObject) JSC.Maybe(ArrayBuffer) { - if (this.done) { - return .{ .result = ArrayBuffer.fromBytes(&[_]u8{}, .ArrayBuffer) }; - } - - std.debug.assert(this.next == null); - var list = this.bytes.listManaged(this.allocator); - this.bytes = bun.ByteList.init(""); - this.done = true; - this.signal.close(null); - return .{ .result = ArrayBuffer.fromBytes( - list.toOwnedSlice() catch @panic("TODO"), - if (this.as_uint8array) - .Uint8Array - else - .ArrayBuffer, - ) }; - } - - pub fn sink(this: *ArrayBufferSink) Sink { - return Sink.init(this); - } - - pub const JSSink = NewJSSink(@This(), "ArrayBufferSink"); -}; - -pub const UVStreamSink = struct { - stream: StreamType, - - allocator: std.mem.Allocator, - done: bool = false, - signal: Signal = .{}, - next: ?Sink = null, - buffer: bun.ByteList = .{}, - closeCallback: CloseCallbackHandler = CloseCallbackHandler.Empty, - deinit_onclose: bool = false, - pub const name = "UVStreamSink"; - const StreamType = if (Environment.isWindows) ?*uv.uv_stream_t else ?*anyopaque; - - pub const CloseCallbackHandler = struct { - ctx: ?*anyopaque = null, - callback: ?*const fn (ctx: ?*anyopaque) void = null, - - pub const Empty: CloseCallbackHandler = .{}; - - pub fn init(ctx: *anyopaque, callback: *const fn (ctx: ?*anyopaque) void) CloseCallbackHandler { - return CloseCallbackHandler{ - .ctx = ctx, - .callback = callback, - }; - } - - pub fn run(this: *const CloseCallbackHandler) void { - if (this.callback) |callback| { - callback(this.ctx); - } - } - }; - - const AsyncWriteInfo = struct { - sink: *UVStreamSink, - input_buffer: uv.uv_buf_t = std.mem.zeroes(uv.uv_buf_t), - req: uv.uv_write_t = std.mem.zeroes(uv.uv_write_t), - - pub fn init(parent: *UVStreamSink, data: []const u8) *AsyncWriteInfo { - var info = bun.new(AsyncWriteInfo, .{ .sink = parent }); - info.req.data = info; - info.input_buffer = uv.uv_buf_t.init(bun.default_allocator.dupe(u8, data) catch bun.outOfMemory()); - return info; - } - - fn uvWriteCallback(req: *uv.uv_write_t, status: uv.ReturnCode) callconv(.C) void { - const this = bun.cast(*AsyncWriteInfo, req.data); - defer this.deinit(); - if (status.errEnum()) |err| { - _ = this.sink.end(bun.sys.Error.fromCode(err, .write)); - return; - } - } - - pub fn run(this: *AsyncWriteInfo) void { - if (this.sink.stream) |stream| { - if (uv.uv_write(&this.req, @ptrCast(stream), @ptrCast(&this.input_buffer), 1, AsyncWriteInfo.uvWriteCallback).errEnum()) |err| { - _ = this.sink.end(bun.sys.Error.fromCode(err, .write)); - this.deinit(); - } - } - } - - pub fn deinit(this: *AsyncWriteInfo) void { - bun.default_allocator.free(this.input_buffer.slice()); - bun.default_allocator.destroy(this); - } - }; - - fn writeAsync(this: *UVStreamSink, data: []const u8) void { - if (this.done) return; - if (!Environment.isWindows) @panic("UVStreamSink is only supported on Windows"); - - AsyncWriteInfo.init(this, data).run(); - } - - fn writeMaybeSync(this: *UVStreamSink, data: []const u8) void { - if (!Environment.isWindows) @panic("UVStreamSink is only supported on Windows"); - - if (this.done) return; - - var to_write = data; - while (to_write.len > 0) { - const stream = this.stream orelse return; - var input_buffer = uv.uv_buf_t.init(to_write); - const status = uv.uv_try_write(@ptrCast(stream), @ptrCast(&input_buffer), 1); - if (status.errEnum()) |err| { - if (err == bun.C.E.AGAIN) { - this.writeAsync(to_write); - return; - } - _ = this.end(bun.sys.Error.fromCode(err, .write)); - return; - } - const bytes_written: usize = @intCast(status.int()); - to_write = to_write[bytes_written..]; - } - } - - pub fn connect(this: *UVStreamSink, signal: Signal) void { - std.debug.assert(this.reader == null); - this.signal = signal; - } - - pub fn start(this: *UVStreamSink, _: StreamStart) JSC.Maybe(void) { - this.done = false; - this.signal.start(); - return .{ .result = {} }; - } - - pub fn flush(_: *UVStreamSink) JSC.Maybe(void) { - return .{ .result = {} }; - } - - pub fn flushFromJS(_: *UVStreamSink, _: *JSGlobalObject, _: bool) JSC.Maybe(JSValue) { - return .{ .result = JSValue.jsNumber(0) }; - } - - fn uvCloseCallback(handler: *anyopaque) callconv(.C) void { - const event = bun.cast(*uv.uv_pipe_t, handler); - var this = bun.cast(*UVStreamSink, event.data); - this.stream = null; - if (this.deinit_onclose) { - this._destroy(); - } - } - - pub fn isClosed(this: *UVStreamSink) bool { - const stream = this.stream orelse return true; - return uv.uv_is_closed(@ptrCast(stream)); - } - - pub fn close(this: *UVStreamSink) void { - if (!Environment.isWindows) @panic("UVStreamSink is only supported on Windows"); - const stream = this.stream orelse return; - stream.data = this; - if (this.isClosed()) { - this.stream = null; - if (this.deinit_onclose) { - this._destroy(); - } - } else { - _ = uv.uv_close(@ptrCast(stream), UVStreamSink.uvCloseCallback); - } - } - - fn _destroy(this: *UVStreamSink) void { - const callback = this.closeCallback; - defer callback.run(); - this.stream = null; - if (this.buffer.cap > 0) { - this.buffer.listManaged(this.allocator).deinit(); - this.buffer = bun.ByteList.init(""); - } - this.allocator.destroy(this); - } - - pub fn finalize(this: *UVStreamSink) void { - if (this.stream == null) { - this._destroy(); - } else { - this.deinit_onclose = true; - this.close(); - } - } - - pub fn init(allocator: std.mem.Allocator, stream: StreamType, next: ?Sink) !*UVStreamSink { - const this = try allocator.create(UVStreamSink); - this.* = UVStreamSink{ - .stream = stream, - .allocator = allocator, - .next = next, - }; - return this; - } - - pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeBytes(data); - } - const bytes = data.slice(); - this.writeMaybeSync(bytes); - this.signal.ready(null, null); - return .{ .owned = @truncate(bytes.len) }; - } - - pub const writeBytes = write; - pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeLatin1(data); - } - const bytes = data.slice(); - if (strings.isAllASCII(bytes)) { - this.writeMaybeSync(bytes); - this.signal.ready(null, null); - return .{ .owned = @truncate(bytes.len) }; - } - this.buffer.len = 0; - const len = this.buffer.writeLatin1(this.allocator, bytes) catch { - return .{ .err = Syscall.Error.fromCode(.NOMEM, .write) }; - }; - this.writeMaybeSync(this.buffer.slice()); - this.signal.ready(null, null); - return .{ .owned = len }; - } - - pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { - if (this.next) |*next| { - return next.writeUTF16(data); - } - this.buffer.len = 0; - const len = this.buffer.writeUTF16(this.allocator, @as([*]const u16, @ptrCast(@alignCast(data.slice().ptr)))[0..std.mem.bytesAsSlice(u16, data.slice()).len]) catch { - return .{ .err = Syscall.Error.oom }; - }; - this.writeMaybeSync(this.buffer.slice()); - this.signal.ready(null, null); - return .{ .owned = len }; - } - - pub fn end(this: *UVStreamSink, err: ?Syscall.Error) JSC.Maybe(void) { - if (this.next) |*next| { - return next.end(err); - } - this.close(); - this.signal.close(err); - return .{ .result = {} }; - } - - pub fn destroy(this: *UVStreamSink) void { - if (this.stream == null) { - this._destroy(); - } else { - this.deinit_onclose = true; - this.close(); - } - } - - pub fn toJS(this: *UVStreamSink, globalThis: *JSGlobalObject) JSValue { - return JSSink.createObject(globalThis, this); - } - - pub fn endFromJS(this: *UVStreamSink, _: *JSGlobalObject) JSC.Maybe(JSValue) { - if (this.done) { - return .{ .result = JSC.JSValue.jsNumber(0) }; - } - this.close(); - std.debug.assert(this.next == null); - - if (this.buffer.cap > 0) { - this.buffer.listManaged(this.allocator).deinit(); - this.buffer = bun.ByteList.init(""); - } - this.done = true; - this.signal.close(null); - return .{ .result = JSC.JSValue.jsNumber(0) }; - } - - pub fn sink(this: *UVStreamSink) Sink { - return Sink.init(this); - } - - pub const JSSink = NewJSSink(@This(), "UVStreamSink"); -}; - -const AutoFlusher = struct { - registered: bool = false, - - pub fn registerDeferredMicrotaskWithType(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { - if (this.auto_flusher.registered) return; - this.auto_flusher.registered = true; - std.debug.assert(!vm.eventLoop().registerDeferredTask(this, @ptrCast(&Type.onAutoFlush))); - } - - pub fn unregisterDeferredMicrotaskWithType(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { - if (!this.auto_flusher.registered) return; - this.auto_flusher.registered = false; - std.debug.assert(vm.eventLoop().unregisterDeferredTask(this)); - } - - pub fn unregisterDeferredMicrotaskWithTypeUnchecked(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { - std.debug.assert(this.auto_flusher.registered); - std.debug.assert(vm.eventLoop().unregisterDeferredTask(this)); - this.auto_flusher.registered = false; - } - - pub fn registerDeferredMicrotaskWithTypeUnchecked(comptime Type: type, this: *Type, vm: *JSC.VirtualMachine) void { - std.debug.assert(!this.auto_flusher.registered); - this.auto_flusher.registered = true; - std.debug.assert(!vm.eventLoop().registerDeferredTask(this, @ptrCast(&Type.onAutoFlush))); - } -}; - -pub fn NewJSSink(comptime SinkType: type, comptime name_: []const u8) type { - return struct { - sink: SinkType, - - const ThisSink = @This(); - - pub const shim = JSC.Shimmer("", name_, @This()); - pub const name = std.fmt.comptimePrint("{s}", .{name_}); - - // This attaches it to JS - pub const SinkSignal = extern struct { - cpp: JSValue, - - pub fn init(cpp: JSValue) Signal { - // this one can be null - @setRuntimeSafety(false); - return Signal.initWithType(SinkSignal, @as(*SinkSignal, @ptrFromInt(@as(usize, @bitCast(@intFromEnum(cpp)))))); - } - - pub fn close(this: *@This(), _: ?Syscall.Error) void { - onClose(@as(SinkSignal, @bitCast(@intFromPtr(this))).cpp, JSValue.jsUndefined()); - } - - pub fn ready(this: *@This(), _: ?Blob.SizeType, _: ?Blob.SizeType) void { - onReady(@as(SinkSignal, @bitCast(@intFromPtr(this))).cpp, JSValue.jsUndefined(), JSValue.jsUndefined()); - } - - pub fn start(_: *@This()) void {} - }; - - pub fn onClose(ptr: JSValue, reason: JSValue) callconv(.C) void { - JSC.markBinding(@src()); - - return shim.cppFn("onClose", .{ ptr, reason }); - } - - pub fn onReady(ptr: JSValue, amount: JSValue, offset: JSValue) callconv(.C) void { - JSC.markBinding(@src()); - - return shim.cppFn("onReady", .{ ptr, amount, offset }); - } - - pub fn onStart(ptr: JSValue, globalThis: *JSGlobalObject) callconv(.C) void { - JSC.markBinding(@src()); - - return shim.cppFn("onStart", .{ ptr, globalThis }); - } - - pub fn createObject(globalThis: *JSGlobalObject, object: *anyopaque) callconv(.C) JSValue { - JSC.markBinding(@src()); - - return shim.cppFn("createObject", .{ globalThis, object }); - } - - pub fn fromJS(globalThis: *JSGlobalObject, value: JSValue) ?*anyopaque { - JSC.markBinding(@src()); - - return shim.cppFn("fromJS", .{ globalThis, value }); - } - - pub fn construct(globalThis: *JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - if (comptime !@hasDecl(SinkType, "construct")) { - const Static = struct { - pub const message = std.fmt.comptimePrint("{s} is not constructable", .{SinkType.name}); - }; - const err = JSC.SystemError{ - .message = bun.String.static(Static.message), - .code = bun.String.static(@as(string, @tagName(JSC.Node.ErrorCode.ERR_ILLEGAL_CONSTRUCTOR))), - }; - globalThis.throwValue(err.toErrorInstance(globalThis)); - return JSC.JSValue.jsUndefined(); - } - - var allocator = globalThis.bunVM().allocator; - var this = allocator.create(ThisSink) catch { - globalThis.vm().throwError(globalThis, Syscall.Error.oom.toJSC( - globalThis, - )); - return JSC.JSValue.jsUndefined(); - }; - this.sink.construct(allocator); - return createObject(globalThis, this); - } - - pub fn finalize(ptr: *anyopaque) callconv(.C) void { - var this = @as(*ThisSink, @ptrCast(@alignCast(ptr))); - - this.sink.finalize(); - } - - pub fn detach(this: *ThisSink) void { - if (comptime !@hasField(SinkType, "signal")) - return; - - const ptr = this.sink.signal.ptr; - if (this.sink.signal.isDead()) - return; - this.sink.signal.clear(); - const value = @as(JSValue, @enumFromInt(@as(JSC.JSValueReprInt, @bitCast(@intFromPtr(ptr))))); - value.unprotect(); - detachPtr(value); - } - - pub fn detachPtr(ptr: JSValue) callconv(.C) void { - shim.cppFn("detachPtr", .{ptr}); - } - - fn getThis(globalThis: *JSGlobalObject, callframe: *const JSC.CallFrame) ?*ThisSink { - return @as( - *ThisSink, - @ptrCast(@alignCast( - fromJS( - globalThis, - callframe.this(), - ) orelse return null, - )), - ); - } - - fn invalidThis(globalThis: *JSGlobalObject) JSValue { - const err = JSC.toTypeError(JSC.Node.ErrorCode.ERR_INVALID_THIS, "Expected Sink", .{}, globalThis); - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - - pub fn write(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - const args_list = callframe.arguments(4); - const args = args_list.ptr[0..args_list.len]; - - if (args.len == 0) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - JSC.Node.ErrorCode.ERR_MISSING_ARGS, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return JSC.JSValue.jsUndefined(); - } - - const arg = args[0]; - arg.ensureStillAlive(); - defer arg.ensureStillAlive(); - - if (arg.isEmptyOrUndefinedOrNull()) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - JSC.Node.ErrorCode.ERR_STREAM_NULL_VALUES, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return JSC.JSValue.jsUndefined(); - } - - if (arg.asArrayBuffer(globalThis)) |buffer| { - const slice = buffer.slice(); - if (slice.len == 0) { - return JSC.JSValue.jsNumber(0); - } - - return this.sink.writeBytes(.{ .temporary = bun.ByteList.init(slice) }).toJS(globalThis); - } - - if (!arg.isString()) { - globalThis.vm().throwError(globalThis, JSC.toTypeError( - JSC.Node.ErrorCode.ERR_INVALID_ARG_TYPE, - "write() expects a string, ArrayBufferView, or ArrayBuffer", - .{}, - globalThis, - )); - return JSC.JSValue.jsUndefined(); - } - - const str = arg.getZigString(globalThis); - if (str.len == 0) { - return JSC.JSValue.jsNumber(0); - } - - if (str.is16Bit()) { - return this.sink.writeUTF16(.{ .temporary = bun.ByteList.initConst(std.mem.sliceAsBytes(str.utf16SliceAligned())) }).toJS(globalThis); - } - - return this.sink.writeLatin1(.{ .temporary = bun.ByteList.initConst(str.slice()) }).toJS(globalThis); - } - - pub fn writeUTF8(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - const args_list = callframe.arguments(4); - const args = args_list.ptr[0..args_list.len]; - if (args.len == 0 or !args[0].isString()) { - const err = JSC.toTypeError( - if (args.len == 0) JSC.Node.ErrorCode.ERR_MISSING_ARGS else JSC.Node.ErrorCode.ERR_INVALID_ARG_TYPE, - "writeUTF8() expects a string", - .{}, - globalThis, - ); - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - - const arg = args[0]; - - const str = arg.getZigString(globalThis); - if (str.len == 0) { - return JSC.JSValue.jsNumber(0); - } - - if (str.is16Bit()) { - return this.sink.writeUTF16(.{ .temporary = str.utf16SliceAligned() }).toJS(globalThis); - } - - return this.sink.writeLatin1(.{ .temporary = str.slice() }).toJS(globalThis); - } - - pub fn close(globalThis: *JSGlobalObject, sink_ptr: ?*anyopaque) callconv(.C) JSValue { - JSC.markBinding(@src()); - var this = @as(*ThisSink, @ptrCast(@alignCast(sink_ptr orelse return invalidThis(globalThis)))); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - return this.sink.end(null).toJS(globalThis); - } - - pub fn flush(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - defer { - if ((comptime @hasField(SinkType, "done")) and this.sink.done) { - callframe.this().unprotect(); - } - } - - if (comptime @hasDecl(SinkType, "flushFromJS")) { - const wait = callframe.argumentsCount() > 0 and - callframe.argument(0).isBoolean() and - callframe.argument(0).asBoolean(); - const maybe_value: JSC.Maybe(JSValue) = this.sink.flushFromJS(globalThis, wait); - return switch (maybe_value) { - .result => |value| value, - .err => |err| blk: { - globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - break :blk JSC.JSValue.jsUndefined(); - }, - }; - } - - return this.sink.flush().toJS(globalThis); - } - - pub fn start(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - if (comptime @hasField(StreamStart, name_)) { - return this.sink.start( - if (callframe.argumentsCount() > 0) - StreamStart.fromJSWithTag( - globalThis, - callframe.argument(0), - comptime @field(StreamStart, name_), - ) - else - StreamStart{ .empty = {} }, - ).toJS(globalThis); - } - - return this.sink.start( - if (callframe.argumentsCount() > 0) - StreamStart.fromJS(globalThis, callframe.argument(0)) - else - StreamStart{ .empty = {} }, - ).toJS(globalThis); - } - - pub fn end(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - JSC.markBinding(@src()); - - var this = getThis(globalThis, callframe) orelse return invalidThis(globalThis); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - defer { - if (comptime @hasField(SinkType, "done")) { - if (this.sink.done) { - callframe.this().unprotect(); - } - } - } - - return this.sink.endFromJS(globalThis).toJS(globalThis); - } - - pub fn endWithSink(ptr: *anyopaque, globalThis: *JSGlobalObject) callconv(.C) JSValue { - JSC.markBinding(@src()); - - var this = @as(*ThisSink, @ptrCast(@alignCast(ptr))); - - if (comptime @hasDecl(SinkType, "getPendingError")) { - if (this.sink.getPendingError()) |err| { - globalThis.vm().throwError(globalThis, err); - return JSC.JSValue.jsUndefined(); - } - } - - return this.sink.endFromJS(globalThis).toJS(globalThis); - } - - pub fn assignToStream(globalThis: *JSGlobalObject, stream: JSValue, ptr: *anyopaque, jsvalue_ptr: **anyopaque) JSValue { - return shim.cppFn("assignToStream", .{ globalThis, stream, ptr, jsvalue_ptr }); - } - - pub const Export = shim.exportFunctions(.{ - .finalize = finalize, - .write = write, - .close = close, - .flush = flush, - .start = start, - .end = end, - .construct = construct, - .endWithSink = endWithSink, - .updateRef = updateRef, - }); - - pub fn updateRef(ptr: *anyopaque, value: bool) callconv(.C) void { - JSC.markBinding(@src()); - var this = bun.cast(*ThisSink, ptr); - if (comptime @hasDecl(SinkType, "updateRef")) - this.sink.updateRef(value); - } - - comptime { - if (!JSC.is_bindgen) { - @export(finalize, .{ .name = Export[0].symbol_name }); - @export(write, .{ .name = Export[1].symbol_name }); - @export(close, .{ .name = Export[2].symbol_name }); - @export(flush, .{ .name = Export[3].symbol_name }); - @export(start, .{ .name = Export[4].symbol_name }); - @export(end, .{ .name = Export[5].symbol_name }); - @export(construct, .{ .name = Export[6].symbol_name }); - @export(endWithSink, .{ .name = Export[7].symbol_name }); - @export(updateRef, .{ .name = Export[8].symbol_name }); - } + comptime { + if (!JSC.is_bindgen) { + @export(finalize, .{ .name = Export[0].symbol_name }); + @export(write, .{ .name = Export[1].symbol_name }); + @export(close, .{ .name = Export[2].symbol_name }); + @export(flush, .{ .name = Export[3].symbol_name }); + @export(start, .{ .name = Export[4].symbol_name }); + @export(end, .{ .name = Export[5].symbol_name }); + @export(construct, .{ .name = Export[6].symbol_name }); + @export(endWithSink, .{ .name = Export[7].symbol_name }); + @export(updateRef, .{ .name = Export[8].symbol_name }); + } } pub const Extern = [_][]const u8{ "createObject", "fromJS", "assignToStream", "onReady", "onClose", "detachPtr" }; @@ -3386,17 +2551,20 @@ pub fn ReadableStreamSource( return struct { context: Context, cancelled: bool = false, - deinited: bool = false, ref_count: u32 = 1, pending_err: ?Syscall.Error = null, - close_handler: ?*const fn (*anyopaque) void = null, + close_handler: ?*const fn (?*anyopaque) void = null, close_ctx: ?*anyopaque = null, - close_jsvalue: JSValue = JSValue.zero, + close_jsvalue: JSC.Strong = .{}, globalThis: *JSGlobalObject = undefined, + this_jsvalue: JSC.JSValue = .zero, + is_closed: bool = false, const This = @This(); const ReadableStreamSourceType = @This(); + pub usingnamespace bun.New(@This()); + pub fn pull(this: *This, buf: []u8) StreamResult { return onPull(&this.context, buf, JSValue.zero); } @@ -3425,16 +2593,16 @@ pub fn ReadableStreamSource( return onStart(&this.context); } - pub fn pullFromJS(this: *This, buf: []u8, view: JSValue) StreamResult { + pub fn onPullFromJS(this: *This, buf: []u8, view: JSValue) StreamResult { return onPull(&this.context, buf, view); } - pub fn startFromJS(this: *This) StreamStart { + pub fn onStartFromJS(this: *This) StreamStart { return onStart(&this.context); } pub fn cancel(this: *This) void { - if (this.cancelled or this.deinited) { + if (this.cancelled) { return; } @@ -3443,31 +2611,34 @@ pub fn ReadableStreamSource( } pub fn onClose(this: *This) void { - if (this.cancelled or this.deinited) { + if (this.cancelled) { return; } if (this.close_handler) |close| { this.close_handler = null; - close(this.close_ctx); + if (close == &JSReadableStreamSource.onClose) { + JSReadableStreamSource.onClose(this); + } else { + close(this.close_ctx); + } } } - pub fn incrementCount(this: *This) !void { - if (this.deinited) { - return error.InvalidStream; - } + pub fn incrementCount(this: *This) void { this.ref_count += 1; } pub fn decrementCount(this: *This) u32 { - if (this.ref_count == 0 or this.deinited) { - return 0; + if (comptime Environment.isDebug) { + if (this.ref_count == 0) { + @panic("Attempted to decrement ref count below zero"); + } } this.ref_count -= 1; if (this.ref_count == 0) { - this.deinited = true; + this.close_jsvalue.deinit(); deinit_fn(&this.context); return 0; } @@ -3492,35 +2663,61 @@ pub fn ReadableStreamSource( return .{}; } - pub fn toJS(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject) JSC.JSValue { - return ReadableStream.fromNative(globalThis, Context.tag, this); + pub fn toReadableStream(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject) JSC.JSValue { + const out_value = brk: { + if (this.this_jsvalue != .zero) { + break :brk this.this_jsvalue; + } + + break :brk this.toJS(globalThis); + }; + out_value.ensureStillAlive(); + this.this_jsvalue = out_value; + return ReadableStream.fromNative(globalThis, out_value); } const supports_ref = setRefUnrefFn != null; + pub usingnamespace @field(JSC.Codegen, "JS" ++ name_ ++ "InternalReadableStreamSource"); + pub const drainFromJS = JSReadableStreamSource.drain; + pub const startFromJS = JSReadableStreamSource.start; + pub const pullFromJS = JSReadableStreamSource.pull; + pub const cancelFromJS = JSReadableStreamSource.cancel; + pub const updateRefFromJS = JSReadableStreamSource.updateRef; + pub const setOnCloseFromJS = JSReadableStreamSource.setOnCloseFromJS; + pub const getOnCloseFromJS = JSReadableStreamSource.getOnCloseFromJS; + pub const setOnDrainFromJS = JSReadableStreamSource.setOnDrainFromJS; + pub const getOnDrainFromJS = JSReadableStreamSource.getOnDrainFromJS; + pub const finalize = JSReadableStreamSource.finalize; + pub const construct = JSReadableStreamSource.construct; + pub const getIsClosedFromJS = JSReadableStreamSource.isClosed; pub const JSReadableStreamSource = struct { - pub const shim = JSC.Shimmer(name_, "JSReadableStreamSource", @This()); - pub const name = std.fmt.comptimePrint("{s}_JSReadableStreamSource", .{name_}); + pub fn construct(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) ?*ReadableStreamSourceType { + _ = callFrame; // autofix + globalThis.throw("Cannot construct ReadableStreamSource", .{}); + return null; + } - pub fn pull(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn pull(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - const arguments = callFrame.arguments(3); - var this = arguments.ptr[0].asPtr(ReadableStreamSourceType); - const view = arguments.ptr[1]; + const this_jsvalue = callFrame.this(); + const arguments = callFrame.arguments(2); + const view = arguments.ptr[0]; view.ensureStillAlive(); - this.globalThis = globalThis; + this.this_jsvalue = this_jsvalue; var buffer = view.asArrayBuffer(globalThis) orelse return JSC.JSValue.jsUndefined(); return processResult( + this_jsvalue, globalThis, - arguments.ptr[2], - this.pullFromJS(buffer.slice(), view), + arguments.ptr[1], + this.onPullFromJS(buffer.slice(), view), ); } - pub fn start(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn start(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); this.globalThis = globalThis; - switch (this.startFromJS()) { + this.this_jsvalue = callFrame.this(); + switch (this.onStartFromJS()) { .empty => return JSValue.jsNumber(0), .ready => return JSValue.jsNumber(16384), .chunk_size => |size| return JSValue.jsNumber(size), @@ -3528,11 +2725,18 @@ pub fn ReadableStreamSource( globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); return JSC.JSValue.jsUndefined(); }, - else => unreachable, + else => |rc| { + return rc.toJS(globalThis); + }, } } - pub fn processResult(globalThis: *JSGlobalObject, flags: JSValue, result: StreamResult) JSC.JSValue { + pub fn isClosed(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + _ = globalObject; // autofix + return JSC.JSValue.jsBoolean(this.is_closed); + } + + fn processResult(this_jsvalue: JSC.JSValue, globalThis: *JSGlobalObject, flags: JSValue, result: StreamResult) JSC.JSValue { switch (result) { .err => |err| { if (err == .Error) { @@ -3545,6 +2749,11 @@ pub fn ReadableStreamSource( } return JSValue.jsUndefined(); }, + .pending => { + const out = result.toJS(globalThis); + ReadableStreamSourceType.pendingPromiseSetCached(this_jsvalue, globalThis, out); + return out; + }, .temporary_and_done, .owned_and_done, .into_array_and_done => { JSC.C.JSObjectSetPropertyAtIndex(globalThis, flags.asObjectRef(), 0, JSValue.jsBoolean(true).asObjectRef(), null); return result.toJS(globalThis); @@ -3552,1585 +2761,1613 @@ pub fn ReadableStreamSource( else => return result.toJS(globalThis), } } - pub fn cancel(_: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn cancel(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + _ = globalObject; // autofix JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); + this.this_jsvalue = callFrame.this(); this.cancel(); return JSC.JSValue.jsUndefined(); } - pub fn setClose(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn setOnCloseFromJS(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(.C) bool { + JSC.markBinding(@src()); + this.close_handler = JSReadableStreamSource.onClose; + this.globalThis = globalObject; + + if (value.isUndefined()) { + this.close_jsvalue.deinit(); + return true; + } + + if (!value.isCallable(globalObject.vm())) { + globalObject.throwInvalidArgumentType("ReadableStreamSource", "onclose", "function"); + return false; + } + const cb = value.withAsyncContextIfNeeded(globalObject); + this.close_jsvalue.set(globalObject, cb); + return true; + } + + pub fn setOnDrainFromJS(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) callconv(.C) bool { + JSC.markBinding(@src()); + this.globalThis = globalObject; + + if (value.isUndefined()) { + ReadableStreamSourceType.onDrainCallbackSetCached(this.this_jsvalue, globalObject, .undefined); + return true; + } + + if (!value.isCallable(globalObject.vm())) { + globalObject.throwInvalidArgumentType("ReadableStreamSource", "onDrain", "function"); + return false; + } + const cb = value.withAsyncContextIfNeeded(globalObject); + ReadableStreamSourceType.onDrainCallbackSetCached(this.this_jsvalue, globalObject, cb); + return true; + } + + pub fn getOnCloseFromJS(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + _ = globalObject; // autofix + + JSC.markBinding(@src()); + + return this.close_jsvalue.get() orelse .undefined; + } + + pub fn getOnDrainFromJS(this: *ReadableStreamSourceType, globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue { + _ = globalObject; // autofix + JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); - this.close_ctx = this; - this.close_handler = JSReadableStreamSource.onClose; - this.globalThis = globalThis; - this.close_jsvalue = callFrame.argument(1); - return JSC.JSValue.jsUndefined(); + + if (ReadableStreamSourceType.onDrainCallbackGetCached(this.this_jsvalue)) |val| { + return val; + } + + return .undefined; } - pub fn updateRef(_: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn updateRef(this: *ReadableStreamSourceType, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); - const ref_or_unref = callFrame.argument(1).asBoolean(); + this.this_jsvalue = callFrame.this(); + const ref_or_unref = callFrame.argument(0).toBooleanSlow(globalObject); this.setRef(ref_or_unref); + return JSC.JSValue.jsUndefined(); } - fn onClose(ptr: *anyopaque) void { + fn onClose(ptr: ?*anyopaque) void { JSC.markBinding(@src()); - var this = bun.cast(*ReadableStreamSourceType, ptr); - _ = this.close_jsvalue.call(this.globalThis, &.{}); - // this.closer + var this = bun.cast(*ReadableStreamSourceType, ptr.?); + if (this.close_jsvalue.trySwap()) |cb| { + this.globalThis.queueMicrotask(cb, &.{}); + } + + this.close_jsvalue.clear(); } - pub fn deinit(_: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); + pub fn finalize(this: *ReadableStreamSourceType) callconv(.C) void { + this.this_jsvalue = .zero; + _ = this.decrementCount(); - return JSValue.jsUndefined(); } - pub fn drain(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { + pub fn drain(this: *ReadableStreamSourceType, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) callconv(.C) JSC.JSValue { JSC.markBinding(@src()); - var this = callFrame.argument(0).asPtr(ReadableStreamSourceType); + this.this_jsvalue = callFrame.this(); var list = this.drain(); if (list.len > 0) { return JSC.ArrayBuffer.fromBytes(list.slice(), .Uint8Array).toJS(globalThis, null); } return JSValue.jsUndefined(); } - - pub fn load(globalThis: *JSGlobalObject) callconv(.C) JSC.JSValue { - JSC.markBinding(@src()); - // This is used also in Node.js streams - return JSC.JSArray.from(globalThis, &.{ - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.pull, true), - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.start, true), - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.cancel, true), - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.setClose, true), - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.deinit, true), - if (supports_ref) - JSC.NewFunction(globalThis, null, 2, JSReadableStreamSource.updateRef, true) - else - JSC.JSValue.jsNull(), - if (drainInternalBuffer != null) - JSC.NewFunction(globalThis, null, 1, JSReadableStreamSource.drain, true) - else - JSC.JSValue.jsNull(), - }); - } - - pub const Export = shim.exportFunctions(.{ - .load = load, - }); - - comptime { - if (!JSC.is_bindgen) { - @export(load, .{ .name = Export[0].symbol_name }); - } - } }; }; } -pub const ByteBlobLoader = struct { - offset: Blob.SizeType = 0, - store: *Blob.Store, - chunk_size: Blob.SizeType = 1024 * 1024 * 2, - remain: Blob.SizeType = 1024 * 1024 * 2, +pub const FileSink = struct { + writer: IOWriter = .{}, + event_loop_handle: JSC.EventLoopHandle, + written: usize = 0, + ref_count: u32 = 1, + pending: StreamResult.Writable.Pending = .{ + .result = .{ .done = {} }, + }, + signal: Signal = Signal{}, done: bool = false, + started: bool = false, + must_be_kept_alive_until_eof: bool = false, - pub const tag = ReadableStream.Tag.Blob; - - pub fn parent(this: *@This()) *Source { - return @fieldParentPtr(Source, "context", this); - } + // TODO: these fields are duplicated on writer() + // we should not duplicate these fields... + pollable: bool = false, + nonblocking: bool = false, + is_socket: bool = false, + fd: bun.FileDescriptor = bun.invalid_fd, + has_js_called_unref: bool = false, - pub fn setup( - this: *ByteBlobLoader, - blob: *const Blob, - user_chunk_size: Blob.SizeType, - ) void { - blob.store.?.ref(); - var blobe = blob.*; - blobe.resolveSize(); - this.* = ByteBlobLoader{ - .offset = blobe.offset, - .store = blobe.store.?, - .chunk_size = if (user_chunk_size > 0) @min(user_chunk_size, blobe.size) else @min(1024 * 1024 * 2, blobe.size), - .remain = blobe.size, - .done = false, - }; - } + const log = Output.scoped(.FileSink, false); - pub fn onStart(this: *ByteBlobLoader) StreamStart { - return .{ .chunk_size = this.chunk_size }; - } + pub usingnamespace bun.NewRefCounted(FileSink, deinit); - pub fn onPull(this: *ByteBlobLoader, buffer: []u8, array: JSC.JSValue) StreamResult { - array.ensureStillAlive(); - defer array.ensureStillAlive(); - if (this.done) { - return .{ .done = {} }; - } + pub const IOWriter = bun.io.StreamingWriter(@This(), onWrite, onError, onReady, onClose); + pub const Poll = IOWriter; - var temporary = this.store.sharedView(); - temporary = temporary[this.offset..]; + pub fn onAttachedProcessExit(this: *FileSink) void { + log("onAttachedProcessExit()", .{}); + this.done = true; + this.writer.close(); - temporary = temporary[0..@min(buffer.len, @min(temporary.len, this.remain))]; - if (temporary.len == 0) { - this.store.deref(); - this.done = true; - return .{ .done = {} }; - } + this.pending.result = .{ .err = Syscall.Error.fromCode(.PIPE, .write) }; - const copied = @as(Blob.SizeType, @intCast(temporary.len)); + this.runPending(); - this.remain -|= copied; - this.offset +|= copied; - std.debug.assert(buffer.ptr != temporary.ptr); - @memcpy(buffer[0..temporary.len], temporary); - if (this.remain == 0) { - return .{ .into_array_and_done = .{ .value = array, .len = copied } }; + if (this.must_be_kept_alive_until_eof) { + this.must_be_kept_alive_until_eof = false; + this.deref(); } - - return .{ .into_array = .{ .value = array, .len = copied } }; } - pub fn onCancel(_: *ByteBlobLoader) void {} - - pub fn deinit(this: *ByteBlobLoader) void { - if (!this.done) { - this.done = true; - this.store.deref(); - } + fn runPending(this: *FileSink) void { + this.ref(); + defer this.deref(); - bun.default_allocator.destroy(this); + const l = this.eventLoop(); + l.enter(); + defer l.exit(); + this.pending.run(); } - pub fn drain(this: *ByteBlobLoader) bun.ByteList { - var temporary = this.store.sharedView(); - temporary = temporary[this.offset..]; - temporary = temporary[0..@min(16384, @min(temporary.len, this.remain))]; + pub fn onWrite(this: *FileSink, amount: usize, status: bun.io.WriteStatus) void { + log("onWrite({d}, {any})", .{ amount, status }); - const cloned = bun.ByteList.init(temporary).listManaged(bun.default_allocator).clone() catch @panic("Out of memory"); - this.offset +|= @as(Blob.SizeType, @truncate(cloned.items.len)); - this.remain -|= @as(Blob.SizeType, @truncate(cloned.items.len)); + this.written += amount; - return bun.ByteList.fromList(cloned); - } + // TODO: on windows done means ended (no pending data on the buffer) on unix we can still have pending data on the buffer + // we should unify the behaviors to simplify this + const has_pending_data = this.writer.hasPendingData(); + // Only keep the event loop ref'd while there's a pending write in progress. + // If there's no pending write, no need to keep the event loop ref'd. + this.writer.updateRef(this.eventLoop(), has_pending_data); - pub const Source = ReadableStreamSource( - @This(), - "ByteBlob", - onStart, - onPull, - onCancel, - deinit, - null, - drain, - ); -}; + // if we are not done yet and has pending data we just wait so we do not runPending twice + if (status == .pending and has_pending_data) { + if (this.pending.state == .pending) { + this.pending.consumed += @truncate(amount); + } + return; + } -pub const PipeFunction = *const fn (ctx: *anyopaque, stream: StreamResult, allocator: std.mem.Allocator) void; + if (this.pending.state == .pending) { + this.pending.consumed += @truncate(amount); -pub const PathOrFileDescriptor = union(enum) { - path: ZigString.Slice, - fd: bun.FileDescriptor, + // when "done" is true, we will never receive more data. + if (this.done or status == .end_of_file) { + this.pending.result = .{ .owned_and_done = this.pending.consumed }; + } else { + this.pending.result = .{ .owned = this.pending.consumed }; + } - pub fn deinit(this: *const PathOrFileDescriptor) void { - if (this.* == .path) this.path.deinit(); - } -}; + this.runPending(); -pub const Pipe = struct { - ctx: ?*anyopaque = null, - onPipe: ?PipeFunction = null, + // this.done == true means ended was called + const ended_and_done = this.done and status == .end_of_file; - pub fn New(comptime Type: type, comptime Function: anytype) type { - return struct { - pub fn pipe(self: *anyopaque, stream: StreamResult, allocator: std.mem.Allocator) void { - Function(@as(*Type, @ptrCast(@alignCast(self))), stream, allocator); + if (this.done and status == .drained) { + // if we call end/endFromJS and we have some pending returned from .flush() we should call writer.end() + this.writer.end(); + } else if (ended_and_done and !has_pending_data) { + this.writer.close(); } + } - pub fn init(self: *Type) Pipe { - return Pipe{ - .ctx = self, - .onPipe = pipe, - }; + if (status == .end_of_file) { + if (this.must_be_kept_alive_until_eof) { + this.must_be_kept_alive_until_eof = false; + this.deref(); } - }; - } -}; - -pub const ByteStream = struct { - buffer: std.ArrayList(u8) = .{ - .allocator = bun.default_allocator, - .items = &.{}, - .capacity = 0, - }, - has_received_last_chunk: bool = false, - pending: StreamResult.Pending = StreamResult.Pending{ - .result = .{ .done = {} }, - }, - done: bool = false, - pending_buffer: []u8 = &.{}, - pending_value: JSC.Strong = .{}, - offset: usize = 0, - highWaterMark: Blob.SizeType = 0, - pipe: Pipe = .{}, - size_hint: Blob.SizeType = 0, - - pub const tag = ReadableStream.Tag.Bytes; - - pub fn setup(this: *ByteStream) void { - this.* = .{}; - } - - pub fn onStart(this: *@This()) StreamStart { - if (this.has_received_last_chunk and this.buffer.items.len == 0) { - return .{ .empty = {} }; + this.signal.close(null); } + } - if (this.has_received_last_chunk) { - return .{ .chunk_size = @min(1024 * 1024 * 2, this.buffer.items.len) }; - } + pub fn onError(this: *FileSink, err: bun.sys.Error) void { + log("onError({any})", .{err}); + if (this.pending.state == .pending) { + this.pending.result = .{ .err = err }; - if (this.highWaterMark == 0) { - return .{ .ready = {} }; + this.runPending(); } - - return .{ .chunk_size = @max(this.highWaterMark, std.mem.page_size) }; } - pub fn value(this: *@This()) JSValue { - const result = this.pending_value.get() orelse { - return .zero; - }; - this.pending_value.clear(); - return result; - } + pub fn onReady(this: *FileSink) void { + log("onReady()", .{}); - pub fn isCancelled(this: *const @This()) bool { - return @fieldParentPtr(Source, "context", this).cancelled; + this.signal.ready(null, null); } - - pub fn unpipe(this: *@This()) void { - this.pipe.ctx = null; - this.pipe.onPipe = null; - if (!this.parent().deinited) { - this.parent().deinited = true; - bun.default_allocator.destroy(this.parent()); - } + pub fn onClose(this: *FileSink) void { + log("onClose()", .{}); + this.signal.close(null); } - pub fn onData( - this: *@This(), - stream: StreamResult, - allocator: std.mem.Allocator, - ) void { - JSC.markBinding(@src()); - if (this.done) { - if (stream.isDone() and (stream == .owned or stream == .owned_and_done)) { - if (stream == .owned) allocator.free(stream.owned.slice()); - if (stream == .owned_and_done) allocator.free(stream.owned_and_done.slice()); - } - - return; - } - - std.debug.assert(!this.has_received_last_chunk); - this.has_received_last_chunk = stream.isDone(); - - if (this.pipe.ctx != null) { - this.pipe.onPipe.?(this.pipe.ctx.?, stream, allocator); - return; + pub fn createWithPipe( + event_loop_: anytype, + pipe: *uv.Pipe, + ) *FileSink { + if (Environment.isPosix) { + @compileError("FileSink.createWithPipe is only available on Windows"); } - const chunk = stream.slice(); - - if (this.pending.state == .pending) { - std.debug.assert(this.buffer.items.len == 0); - const to_copy = this.pending_buffer[0..@min(chunk.len, this.pending_buffer.len)]; - const pending_buffer_len = this.pending_buffer.len; - std.debug.assert(to_copy.ptr != chunk.ptr); - @memcpy(to_copy, chunk[0..to_copy.len]); - this.pending_buffer = &.{}; - - const is_really_done = this.has_received_last_chunk and to_copy.len <= pending_buffer_len; - - if (is_really_done) { - this.done = true; - - if (to_copy.len == 0) { - if (stream == .err) { - if (stream.err == .Error) { - this.pending.result = .{ .err = .{ .Error = stream.err.Error } }; - } - const js_err = stream.err.JSValue; - js_err.ensureStillAlive(); - js_err.protect(); - this.pending.result = .{ .err = .{ .JSValue = js_err } }; - } else { - this.pending.result = .{ - .done = {}, - }; - } - } else { - this.pending.result = .{ - .into_array_and_done = .{ - .value = this.value(), - .len = @as(Blob.SizeType, @truncate(to_copy.len)), - }, - }; - } - } else { - this.pending.result = .{ - .into_array = .{ - .value = this.value(), - .len = @as(Blob.SizeType, @truncate(to_copy.len)), - }, - }; - } - - const remaining = chunk[to_copy.len..]; - if (remaining.len > 0) - this.append(stream, to_copy.len, allocator) catch @panic("Out of memory while copying request body"); - - this.pending.run(); - return; - } + const evtloop = switch (@TypeOf(event_loop_)) { + JSC.EventLoopHandle => event_loop_, + else => JSC.EventLoopHandle.init(event_loop_), + }; - this.append(stream, 0, allocator) catch @panic("Out of memory while copying request body"); + var this = FileSink.new(.{ + .event_loop_handle = JSC.EventLoopHandle.init(evtloop), + .fd = pipe.fd(), + }); + this.writer.setPipe(pipe); + this.writer.setParent(this); + return this; } - pub fn append( - this: *@This(), - stream: StreamResult, - offset: usize, - allocator: std.mem.Allocator, - ) !void { - const chunk = stream.slice()[offset..]; + pub fn create( + event_loop_: anytype, + fd: bun.FileDescriptor, + ) *FileSink { + const evtloop = switch (@TypeOf(event_loop_)) { + JSC.EventLoopHandle => event_loop_, + else => JSC.EventLoopHandle.init(event_loop_), + }; + var this = FileSink.new(.{ + .event_loop_handle = JSC.EventLoopHandle.init(evtloop), + .fd = fd, + }); + this.writer.setParent(this); + return this; + } - if (this.buffer.capacity == 0) { - switch (stream) { - .owned => |owned| { - this.buffer = owned.listManaged(allocator); - this.offset += offset; - }, - .owned_and_done => |owned| { - this.buffer = owned.listManaged(allocator); - this.offset += offset; - }, - .temporary_and_done, .temporary => { - this.buffer = try std.ArrayList(u8).initCapacity(bun.default_allocator, chunk.len); - this.buffer.appendSliceAssumeCapacity(chunk); + pub fn setup(this: *FileSink, options: *const StreamStart.FileSinkOptions) JSC.Maybe(void) { + // TODO: this should be concurrent. + const fd = switch (switch (options.input_path) { + .path => |path| bun.sys.openA(path.slice(), options.flags(), options.mode), + .fd => |fd_| bun.sys.dupWithFlags(fd_, if (bun.FDTag.get(fd_) == .none) std.os.O.NONBLOCK else 0), + }) { + .err => |err| return .{ .err = err }, + .result => |fd| fd, + }; + + if (comptime Environment.isPosix) { + switch (bun.sys.fstat(fd)) { + .err => |err| { + _ = bun.sys.close(fd); + return .{ .err = err }; }, - .err => { - this.pending.result = .{ .err = stream.err }; + .result => |stat| { + this.pollable = bun.sys.isPollable(stat.mode) or std.os.isatty(fd.int()); + this.fd = fd; + this.is_socket = std.os.S.ISSOCK(stat.mode); + this.nonblocking = this.pollable and switch (options.input_path) { + .path => true, + .fd => |fd_| bun.FDTag.get(fd_) == .none, + }; }, - else => unreachable, } - return; + } else if (comptime Environment.isWindows) { + this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0; + this.fd = fd; + } else { + @compileError("TODO: implement for this platform"); } - switch (stream) { - .temporary_and_done, .temporary => { - try this.buffer.appendSlice(chunk); + switch (this.writer.start( + fd, + this.pollable, + )) { + .err => |err| { + _ = bun.sys.close(fd); + return .{ .err = err }; }, - .err => { - this.pending.result = .{ .err = stream.err }; + .result => { + // Only keep the event loop ref'd while there's a pending write in progress. + // If there's no pending write, no need to keep the event loop ref'd. + this.writer.updateRef(this.eventLoop(), false); + if (comptime Environment.isPosix) { + if (this.nonblocking) { + this.writer.getPoll().?.flags.insert(.nonblocking); + } + + if (this.is_socket) { + this.writer.getPoll().?.flags.insert(.socket); + } else if (this.pollable) { + this.writer.getPoll().?.flags.insert(.fifo); + } + } }, - // We don't support the rest of these yet - else => unreachable, } - } - pub fn setValue(this: *@This(), view: JSC.JSValue) void { - JSC.markBinding(@src()); - this.pending_value.set(this.parent().globalThis, view); + return .{ .result = {} }; } - pub fn parent(this: *@This()) *Source { - return @fieldParentPtr(Source, "context", this); + pub fn loop(this: *FileSink) *Async.Loop { + return this.event_loop_handle.loop(); } - pub fn onPull(this: *@This(), buffer: []u8, view: JSC.JSValue) StreamResult { - JSC.markBinding(@src()); - std.debug.assert(buffer.len > 0); - - if (this.buffer.items.len > 0) { - std.debug.assert(this.value() == .zero); - const to_write = @min( - this.buffer.items.len - this.offset, - buffer.len, - ); - const remaining_in_buffer = this.buffer.items[this.offset..][0..to_write]; + pub fn eventLoop(this: *FileSink) JSC.EventLoopHandle { + return this.event_loop_handle; + } - @memcpy(buffer[0..to_write], this.buffer.items[this.offset..][0..to_write]); + pub fn connect(this: *FileSink, signal: Signal) void { + this.signal = signal; + } - if (this.offset + to_write == this.buffer.items.len) { - this.offset = 0; - this.buffer.items.len = 0; - } else { - this.offset += to_write; - } + pub fn start(this: *FileSink, stream_start: StreamStart) JSC.Maybe(void) { + switch (stream_start) { + .FileSink => |*file| { + switch (this.setup(file)) { + .err => |err| { + return .{ .err = err }; + }, + .result => {}, + } + }, + else => {}, + } - if (this.has_received_last_chunk and remaining_in_buffer.len == 0) { - this.buffer.clearAndFree(); - this.done = true; + this.done = false; + this.started = true; + this.signal.start(); + return .{ .result = {} }; + } - return .{ - .into_array_and_done = .{ - .value = view, - .len = @as(Blob.SizeType, @truncate(to_write)), - }, - }; - } + pub fn flush(_: *FileSink) JSC.Maybe(void) { + return .{ .result = {} }; + } - return .{ - .into_array = .{ - .value = view, - .len = @as(Blob.SizeType, @truncate(to_write)), - }, - }; + pub fn flushFromJS(this: *FileSink, globalThis: *JSGlobalObject, wait: bool) JSC.Maybe(JSValue) { + _ = wait; // autofix + if (this.done or this.pending.state == .pending) { + return .{ .result = JSC.JSValue.jsUndefined() }; } - - if (this.has_received_last_chunk) { - return .{ - .done = {}, - }; + const rc = this.writer.flush(); + switch (rc) { + .done => |written| { + this.written += @truncate(written); + }, + .pending => |written| { + this.written += @truncate(written); + }, + .wrote => |written| { + this.written += @truncate(written); + }, + .err => |err| { + return .{ .err = err }; + }, } + return switch (this.toResult(rc)) { + .err => unreachable, + else => |result| .{ .result = result.toJS(globalThis) }, + }; + } - this.pending_buffer = buffer; - this.setValue(view); + pub fn finalize(this: *FileSink) void { + this.pending.deinit(); + this.deref(); + } - return .{ - .pending = &this.pending, + pub fn init(fd: bun.FileDescriptor, event_loop_handle: anytype) *FileSink { + var this = FileSink.new(.{ + .writer = .{}, + .fd = fd, + .event_loop_handle = JSC.EventLoopHandle.init(event_loop_handle), + }); + this.writer.setParent(this); + + return this; + } + + pub fn construct( + this: *FileSink, + allocator: std.mem.Allocator, + ) void { + _ = allocator; // autofix + this.* = FileSink{ + .event_loop_handle = JSC.EventLoopHandle.init(JSC.VirtualMachine.get().eventLoop()), }; } - pub fn onCancel(this: *@This()) void { - JSC.markBinding(@src()); - const view = this.value(); - if (this.buffer.capacity > 0) this.buffer.clearAndFree(); - this.done = true; - this.pending_value.deinit(); + pub fn write(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.done) { + return .{ .done = {} }; + } - if (view != .zero) { - this.pending_buffer = &.{}; - this.pending.result = .{ .done = {} }; - this.pending.run(); + return this.toResult(this.writer.write(data.slice())); + } + pub const writeBytes = write; + pub fn writeLatin1(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.done) { + return .{ .done = {} }; } + return this.toResult(this.writer.writeLatin1(data.slice())); } + pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { + if (this.done) { + return .{ .done = {} }; + } - pub fn deinit(this: *@This()) void { - JSC.markBinding(@src()); - if (this.buffer.capacity > 0) this.buffer.clearAndFree(); + return this.toResult(this.writer.writeUTF16(data.slice16())); + } - this.pending_value.deinit(); - if (!this.done) { - this.done = true; + pub fn end(this: *FileSink, err: ?Syscall.Error) JSC.Maybe(void) { + if (this.done) { + return .{ .result = {} }; + } - this.pending_buffer = &.{}; - this.pending.result = .{ .done = {} }; - this.pending.run(); + _ = err; // autofix + + switch (this.writer.flush()) { + .done => |written| { + this.written += @truncate(written); + this.writer.end(); + return .{ .result = {} }; + }, + .err => |e| { + this.writer.close(); + return .{ .err = e }; + }, + .pending => |written| { + this.written += @truncate(written); + if (!this.must_be_kept_alive_until_eof) { + this.must_be_kept_alive_until_eof = true; + this.ref(); + } + this.done = true; + return .{ .result = {} }; + }, + .wrote => |written| { + this.written += @truncate(written); + this.writer.end(); + return .{ .result = {} }; + }, } + } + pub fn deinit(this: *FileSink) void { + this.writer.deinit(); + } - bun.default_allocator.destroy(this.parent()); + pub fn toJS(this: *FileSink, globalThis: *JSGlobalObject) JSValue { + return JSSink.createObject(globalThis, this, 0); } - pub const Source = ReadableStreamSource( - @This(), - "ByteStream", - onStart, - onPull, - onCancel, - deinit, - null, - null, - ); -}; + pub fn toJSWithDestructor(this: *FileSink, globalThis: *JSGlobalObject, destructor: ?SinkDestructor.Ptr) JSValue { + return JSSink.createObject(globalThis, this, if (destructor) |dest| @intFromPtr(dest.ptr()) else 0); + } -pub const ReadResult = union(enum) { - pending: void, - err: Syscall.Error, - done: void, - read: []u8, + pub fn endFromJS(this: *FileSink, globalThis: *JSGlobalObject) JSC.Maybe(JSValue) { + if (this.done) { + if (this.pending.state == .pending) { + return .{ .result = this.pending.future.promise.promise.asValue(globalThis) }; + } - pub fn toStream(this: ReadResult, pending: *StreamResult.Pending, buf: []u8, view: JSValue, close_on_empty: bool) StreamResult { - return toStreamWithIsDone( - this, - pending, - buf, - view, - close_on_empty, - false, - ); - } - pub fn toStreamWithIsDone(this: ReadResult, pending: *StreamResult.Pending, buf: []u8, view: JSValue, close_on_empty: bool, is_done: bool) StreamResult { - return switch (this) { - .pending => .{ .pending = pending }, - .err => .{ .err = .{ .Error = this.err } }, - .done => .{ .done = {} }, - .read => |slice| brk: { - const owned = slice.ptr != buf.ptr; - const done = is_done or (close_on_empty and slice.len == 0); + return .{ .result = JSValue.jsNumber(this.written) }; + } - break :brk if (owned and done) - StreamResult{ .owned_and_done = bun.ByteList.init(slice) } - else if (owned) - StreamResult{ .owned = bun.ByteList.init(slice) } - else if (done) - StreamResult{ .into_array_and_done = .{ .len = @as(Blob.SizeType, @truncate(slice.len)), .value = view } } - else - StreamResult{ .into_array = .{ .len = @as(Blob.SizeType, @truncate(slice.len)), .value = view } }; + switch (this.writer.flush()) { + .done => |written| { + this.updateRef(false); + this.writer.end(); + return .{ .result = JSValue.jsNumber(written) }; }, - }; + .err => |err| { + this.writer.close(); + return .{ .err = err }; + }, + .pending => |pending_written| { + this.written += @truncate(pending_written); + if (!this.must_be_kept_alive_until_eof) { + this.must_be_kept_alive_until_eof = true; + this.ref(); + } + this.done = true; + this.pending.result = .{ .owned = @truncate(pending_written) }; + return .{ .result = this.pending.promise(globalThis).asValue(globalThis) }; + }, + .wrote => |written| { + this.writer.end(); + return .{ .result = JSValue.jsNumber(written) }; + }, + } } -}; - -pub const AutoSizer = struct { - buffer: *bun.ByteList, - allocator: std.mem.Allocator, - max: usize, - pub fn resize(this: *AutoSizer, size: usize) ![]u8 { - const available = this.buffer.cap - this.buffer.len; - if (available >= size) return this.buffer.ptr[this.buffer.len..this.buffer.cap][0..size]; - const to_grow = size -| available; - if (to_grow + @as(usize, this.buffer.cap) > this.max) - return this.buffer.ptr[this.buffer.len..this.buffer.cap]; + pub fn sink(this: *FileSink) Sink { + return Sink.init(this); + } - var list = this.buffer.listManaged(this.allocator); - const prev_len = list.items.len; - try list.ensureTotalCapacity(to_grow + @as(usize, this.buffer.cap)); - this.buffer.update(list); - return this.buffer.ptr[prev_len..@as(usize, this.buffer.cap)]; + pub fn updateRef(this: *FileSink, value: bool) void { + this.has_js_called_unref = !value; + if (value) { + this.writer.enableKeepingProcessAlive(this.event_loop_handle); + } else { + this.writer.disableKeepingProcessAlive(this.event_loop_handle); + } } -}; -pub const FIFO = NewFIFO(.js); -pub const FIFOMini = NewFIFO(.mini); + pub const JSSink = NewJSSink(@This(), "FileSink"); -pub fn NewFIFO(comptime EventLoop: JSC.EventLoopKind) type { - return struct { - buf: []u8 = &[_]u8{}, - view: JSC.Strong = .{}, - poll_ref: ?*Async.FilePoll = null, - fd: bun.FileDescriptor = bun.invalid_fd, - to_read: ?u32 = null, - close_on_empty_read: bool = false, - auto_sizer: ?*AutoSizer = null, - pending: StreamResult.Pending = StreamResult.Pending{ - .future = undefined, - .state = .none, - .result = .{ .done = {} }, - }, - signal: JSC.WebCore.Signal = .{}, - is_first_read: bool = true, - has_adjusted_pipe_size_on_linux: bool = false, - drained: bool = true, - - pub const event_loop_kind = EventLoop; - pub usingnamespace NewReadyWatcher(@This(), .readable, ready); - - pub fn finish(this: *@This()) void { - this.close_on_empty_read = true; - if (this.poll_ref) |poll| { - poll.flags.insert(.hup); - poll.disableKeepingProcessAlive(EventLoop.getVm()); - } + fn toResult(this: *FileSink, write_result: bun.io.WriteResult) StreamResult.Writable { + switch (write_result) { + .done => |amt| { + if (amt > 0) + return .{ .owned_and_done = @truncate(amt) }; - this.pending.result = .{ .done = {} }; - this.pending.run(); + return .{ .done = {} }; + }, + .wrote => |amt| { + if (amt > 0) + return .{ .owned = @truncate(amt) }; + + return .{ .temporary = @truncate(amt) }; + }, + .err => |err| { + return .{ .err = err }; + }, + .pending => |pending_written| { + if (!this.must_be_kept_alive_until_eof) { + this.must_be_kept_alive_until_eof = true; + this.ref(); + } + this.pending.consumed += @truncate(pending_written); + this.pending.result = .{ .owned = @truncate(pending_written) }; + return .{ .pending = &this.pending }; + }, } + } +}; - pub fn close(this: *@This()) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); - } +pub const FileReader = struct { + const log = Output.scoped(.FileReader, false); + reader: IOReader = IOReader.init(FileReader), + done: bool = false, + pending: StreamResult.Pending = .{}, + pending_value: JSC.Strong = .{}, + pending_view: []u8 = &.{}, + fd: bun.FileDescriptor = bun.invalid_fd, + started: bool = false, + waiting_for_onReaderDone: bool = false, + event_loop: JSC.EventLoopHandle, + lazy: Lazy = .{ .none = {} }, + buffered: std.ArrayListUnmanaged(u8) = .{}, + read_inside_on_pull: ReadDuringJSOnPullResult = .{ .none = {} }, + highwater_mark: usize = 16384, + has_js_called_unref: bool = false, + + pub const IOReader = bun.io.BufferedReader; + pub const Poll = IOReader; + pub const tag = ReadableStream.Tag.File; - const fd = this.fd; - const signal_close = fd != bun.invalid_fd; - defer if (signal_close) this.signal.close(null); - if (signal_close) { - this.fd = bun.invalid_fd; - _ = bun.sys.close(fd); - } + const ReadDuringJSOnPullResult = union(enum) { + none: void, + js: []u8, + amount_read: usize, + temporary: []const u8, + use_buffered: usize, + }; - this.to_read = null; - this.pending.result = .{ .done = {} }; + pub const Lazy = union(enum) { + none: void, + blob: *Blob.Store, - this.pending.run(); - } + const OpenedFileBlob = struct { + fd: bun.FileDescriptor, + pollable: bool = false, + nonblocking: bool = true, + file_type: bun.io.FileType = .file, + }; - pub fn isClosed(this: *@This()) bool { - return this.fd == bun.invalid_fd; - } + pub fn openFileBlob( + file: *Blob.FileStore, + ) JSC.Maybe(OpenedFileBlob) { + var this = OpenedFileBlob{ .fd = bun.invalid_fd }; + var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - pub fn getAvailableToReadOnLinux(this: *@This()) u32 { - var len: c_int = 0; - const rc: c_int = std.c.ioctl(this.fd.cast(), std.os.linux.T.FIONREAD, @as(*c_int, &len)); - if (rc != 0) { - len = 0; - } + const fd = if (file.pathlike != .path) + // We will always need to close the file descriptor. + switch (Syscall.dupWithFlags(file.pathlike.fd, brk: { + if (comptime Environment.isPosix) { + if (bun.FDTag.get(file.pathlike.fd) == .none and !(file.is_atty orelse false)) { + break :brk std.os.O.NONBLOCK; + } + } - if (len > 0) { - if (this.poll_ref) |poll| { - poll.flags.insert(.readable); - } - } else { - if (this.poll_ref) |poll| { - poll.flags.remove(.readable); + break :brk 0; + })) { + .result => |_fd| if (Environment.isWindows) bun.toLibUVOwnedFD(_fd) else _fd, + .err => |err| { + return .{ .err = err.withFd(file.pathlike.fd) }; + }, } + else switch (Syscall.open(file.pathlike.path.sliceZ(&file_buf), std.os.O.RDONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC, 0)) { + .result => |_fd| _fd, + .err => |err| { + return .{ .err = err.withPath(file.pathlike.path.slice()) }; + }, + }; - return @as(u32, 0); + if (comptime Environment.isPosix) { + if ((file.is_atty orelse false) or (fd.int() < 3 and std.os.isatty(fd.cast())) or (file.pathlike == .fd and bun.FDTag.get(file.pathlike.fd) != .none and std.os.isatty(file.pathlike.fd.cast()))) { + // var termios = std.mem.zeroes(std.os.termios); + // _ = std.c.tcgetattr(fd.cast(), &termios); + // bun.C.cfmakeraw(&termios); + // _ = std.c.tcsetattr(fd.cast(), std.os.TCSA.NOW, &termios); + file.is_atty = true; + } } - return @as(u32, @intCast(@max(len, 0))); - } + if (comptime Environment.isPosix) { + const stat: bun.Stat = switch (Syscall.fstat(fd)) { + .result => |result| result, + .err => |err| { + _ = Syscall.close(fd); + return .{ .err = err }; + }, + }; - pub fn adjustPipeCapacityOnLinux(this: *@This(), current: usize, max: usize) void { - // we do not un-mark it as readable if there's nothing in the pipe - if (!this.has_adjusted_pipe_size_on_linux) { - if (current > 0 and max >= std.mem.page_size * 16) { - this.has_adjusted_pipe_size_on_linux = true; - _ = Syscall.setPipeCapacityOnLinux(this.fd, @min(max * 4, Syscall.getMaxPipeSizeOnLinux())); + if (bun.S.ISDIR(stat.mode)) { + bun.Async.Closer.close(fd, {}); + return .{ .err = Syscall.Error.fromCode(.ISDIR, .fstat) }; } - } - } - pub fn cannotRead(this: *@This(), available: u32) ?ReadResult { - if (comptime Environment.isLinux) { - if (available > 0 and available != std.math.maxInt(u32)) { - return null; + this.pollable = bun.sys.isPollable(stat.mode) or (file.is_atty orelse false); + this.file_type = if (bun.S.ISFIFO(stat.mode)) .pipe else if (bun.S.ISSOCK(stat.mode)) .socket else .file; + this.nonblocking = this.pollable and !(file.is_atty orelse false); + + if (this.nonblocking and this.file_type == .pipe) { + this.file_type = .nonblocking_pipe; } } - if (this.poll_ref) |poll| { - if (comptime Environment.isMac) { - if (available > 0 and available != std.math.maxInt(u32)) { - poll.flags.insert(.readable); - } - } + this.fd = fd; - const is_readable = poll.isReadable(); - if (!is_readable and (this.close_on_empty_read or poll.isHUP())) { - // it might be readable actually - this.close_on_empty_read = true; - switch (bun.isReadable(poll.fd)) { - .ready => { - this.close_on_empty_read = false; - return null; - }, - // we need to read the 0 at the end or else we are not truly done - .hup => { - this.close_on_empty_read = true; - poll.flags.insert(.hup); - return null; - }, - else => {}, - } + return .{ .result = this }; + } + }; - return .done; - } else if (!is_readable and poll.isWatching()) { - // if the file was opened non-blocking - // we don't risk anything by attempting to read it! - if (poll.flags.contains(.nonblocking)) - return null; - - // this happens if we've registered a watcher but we haven't - // ticked the event loop since registering it - switch (bun.isReadable(poll.fd)) { - .ready => { - poll.flags.insert(.readable); - return null; - }, - .hup => { - poll.flags.insert(.hup); - poll.flags.insert(.readable); - return null; + pub fn eventLoop(this: *const FileReader) JSC.EventLoopHandle { + return this.event_loop; + } + + pub fn loop(this: *const FileReader) *Async.Loop { + return this.eventLoop().loop(); + } + + pub fn setup( + this: *FileReader, + fd: bun.FileDescriptor, + ) void { + this.* = FileReader{ + .reader = .{}, + .done = false, + .fd = fd, + }; + + this.event_loop = this.parent().globalThis.bunVM().eventLoop(); + } + + pub fn onStart(this: *FileReader) StreamStart { + this.reader.setParent(this); + const was_lazy = this.lazy != .none; + var pollable = false; + var file_type: bun.io.FileType = .file; + if (this.lazy == .blob) { + switch (this.lazy.blob.data) { + .bytes => @panic("Invalid state in FileReader: expected file "), + .file => |*file| { + defer { + this.lazy.blob.deref(); + this.lazy = .none; + } + switch (Lazy.openFileBlob(file)) { + .err => |err| { + this.fd = bun.invalid_fd; + return .{ .err = err }; }, - else => { - return .pending; + .result => |opened| { + this.fd = opened.fd; + pollable = opened.pollable; + file_type = opened.file_type; + this.reader.flags.nonblocking = opened.nonblocking; + this.reader.flags.pollable = pollable; }, } - } - } - - if (comptime Environment.isLinux) { - if (available == 0) { - std.debug.assert(this.poll_ref == null); - return .pending; - } - } else if (available == std.math.maxInt(@TypeOf(available)) and this.poll_ref == null) { - // we don't know if it's readable or not - return switch (bun.isReadable(this.fd)) { - .hup => { - this.close_on_empty_read = true; - return null; - }, - .ready => null, - else => ReadResult{ .pending = {} }, - }; + }, } - - return null; } - pub fn getAvailableToRead(this: *@This(), size_or_offset: i64) ?u32 { - if (comptime Environment.isLinux) { - return this.getAvailableToReadOnLinux(); + { + const reader_fd = this.reader.getFd(); + if (reader_fd != bun.invalid_fd and this.fd == bun.invalid_fd) { + this.fd = reader_fd; } - - if (size_or_offset != std.math.maxInt(@TypeOf(size_or_offset))) - this.to_read = @as(u32, @intCast(@max(size_or_offset, 0))); - - return this.to_read; } - const log = bun.Output.scoped(.FIFO, false); - pub fn ready(this: *@This(), sizeOrOffset: i64, is_hup: bool) void { - log("FIFO ready", .{}); - if (this.isClosed()) { - if (this.isWatching()) - this.unwatch(this.poll_ref.?.fd); - return; + this.event_loop = JSC.EventLoopHandle.init(this.parent().globalThis.bunVM().eventLoop()); + + if (was_lazy) { + _ = this.parent().incrementCount(); + this.waiting_for_onReaderDone = true; + switch (this.reader.start(this.fd, pollable)) { + .result => {}, + .err => |e| { + return .{ .err = e }; + }, } + } else if (comptime Environment.isPosix) { + if (this.reader.flags.pollable and !this.reader.isDone()) { + this.waiting_for_onReaderDone = true; + _ = this.parent().incrementCount(); + } + } - defer { - if (comptime EventLoop == .js) JSC.VirtualMachine.get().drainMicrotasks(); + if (comptime Environment.isPosix) { + if (file_type == .socket) { + this.reader.flags.socket = true; } - if (comptime Environment.isMac) { - if (sizeOrOffset == 0 and is_hup and this.drained) { - this.close(); - return; + if (this.reader.handle.getPoll()) |poll| { + if (file_type == .socket or this.reader.flags.socket) { + poll.flags.insert(.socket); + } else { + // if it's a TTY, we report it as a fifo + // we want the behavior to be as though it were a blocking pipe. + poll.flags.insert(.fifo); } - } else if (is_hup and this.drained and this.getAvailableToReadOnLinux() == 0) { - this.close(); - return; - } - if (this.buf.len == 0) { - var auto_sizer = this.auto_sizer orelse return; - if (comptime Environment.isMac) { - if (sizeOrOffset > 0) { - this.buf = auto_sizer.resize(@as(usize, @intCast(sizeOrOffset))) catch return; - } else { - this.buf = auto_sizer.resize(8192) catch return; - } + if (this.reader.flags.nonblocking) { + poll.flags.insert(.nonblocking); } } + } - const read_result = this.read( - this.buf, - // On Linux, we end up calling ioctl() twice if we don't do this - if (comptime Environment.isMac) - // i33 holds the same amount of unsigned space as a u32, so we truncate it there before casting - @as(u32, @intCast(@as(i33, @truncate(sizeOrOffset)))) - else - null, - ); + this.started = true; - if (read_result == .read) { - if (this.to_read) |*to_read| { - to_read.* = to_read.* -| @as(u32, @truncate(read_result.read.len)); - } + if (this.reader.isDone()) { + this.consumeReaderBuffer(); + if (this.buffered.items.len > 0) { + const buffered = this.buffered; + this.buffered = .{}; + return .{ .owned_and_done = bun.ByteList.init(buffered.items) }; + } + } else if (comptime Environment.isPosix) { + if (!was_lazy and this.reader.flags.pollable) { + this.reader.read(); } + } - this.pending.result = read_result.toStream( - &this.pending, - this.buf, - this.view.get() orelse .zero, - this.close_on_empty_read, - ); - this.pending.run(); + return .{ .ready = {} }; + } + + pub fn parent(this: *@This()) *Source { + return @fieldParentPtr(Source, "context", this); + } + + pub fn onCancel(this: *FileReader) void { + if (this.done) return; + this.done = true; + this.reader.updateRef(false); + if (!this.reader.isDone()) + this.reader.close(); + } + + pub fn deinit(this: *FileReader) void { + this.buffered.deinit(bun.default_allocator); + this.reader.updateRef(false); + this.reader.deinit(); + this.pending_value.deinit(); + + if (this.lazy != .none) { + this.lazy.blob.deref(); + this.lazy = .none; } - pub fn readFromJS( - this: *@This(), - buf_: []u8, - view: JSValue, - globalThis: *JSC.JSGlobalObject, - ) StreamResult { - if (this.isClosed()) { - return .{ .done = {} }; - } + this.parent().destroy(); + } - if (!this.isWatching()) { - this.watch(this.fd); - } + pub fn onReadChunk(this: *@This(), init_buf: []const u8, state: bun.io.ReadState) bool { + const buf = init_buf; + log("onReadChunk() = {d} ({s})", .{ buf.len, @tagName(state) }); - const read_result = this.read(buf_, this.to_read); - if (read_result == .read and read_result.read.len == 0) { - this.close(); - return .{ .done = {} }; + if (this.done) { + this.reader.close(); + return false; + } + + const hasMore = state != .eof; + + if (this.read_inside_on_pull != .none) { + switch (this.read_inside_on_pull) { + .js => |in_progress| { + if (in_progress.len >= buf.len and !hasMore) { + @memcpy(in_progress[0..buf.len], buf); + this.read_inside_on_pull = .{ .js = in_progress[buf.len..] }; + } else if (in_progress.len > 0 and !hasMore) { + this.read_inside_on_pull = .{ .temporary = buf }; + } else if (hasMore and !bun.isSliceInBuffer(buf, this.buffered.allocatedSlice())) { + this.buffered.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory(); + this.read_inside_on_pull = .{ .use_buffered = buf.len }; + } + }, + .use_buffered => |original| { + this.buffered.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory(); + this.read_inside_on_pull = .{ .use_buffered = buf.len + original }; + }, + .none => unreachable, + else => @panic("Invalid state"), + } + } else if (this.pending.state == .pending) { + if (buf.len == 0) { + this.pending.result = .{ .done = {} }; + this.pending_value.clear(); + this.pending_view = &.{}; + this.reader.buffer().clearAndFree(); + this.pending.run(); + return false; } - if (read_result == .read) { - if (this.to_read) |*to_read| { - to_read.* = to_read.* -| @as(u32, @truncate(read_result.read.len)); + const was_done = this.reader.isDone(); + + if (this.pending_view.len >= buf.len) { + @memcpy(this.pending_view[0..buf.len], buf); + this.reader.buffer().clearRetainingCapacity(); + this.buffered.clearRetainingCapacity(); + + if (was_done) { + this.pending.result = .{ + .into_array_and_done = .{ + .value = this.pending_value.get() orelse .zero, + .len = @truncate(buf.len), + }, + }; + } else { + this.pending.result = .{ + .into_array = .{ + .value = this.pending_value.get() orelse .zero, + .len = @truncate(buf.len), + }, + }; } - } - if (read_result == .pending) { - this.buf = buf_; - this.view.set(globalThis, view); - if (!this.isWatching()) this.watch(this.fd); - std.debug.assert(this.isWatching()); - return .{ .pending = &this.pending }; + this.pending_value.clear(); + this.pending_view = &.{}; + this.pending.run(); + return !was_done; } - return read_result.toStream(&this.pending, buf_, view, this.close_on_empty_read); - } + if (!bun.isSliceInBuffer(buf, this.buffered.allocatedSlice())) { + if (this.reader.isDone()) { + this.pending.result = .{ + .temporary_and_done = bun.ByteList.init(buf), + }; + } else { + this.pending.result = .{ + .temporary = bun.ByteList.init(buf), + }; + } - pub fn read( - this: *@This(), - buf_: []u8, - /// provided via kqueue(), only on macOS - kqueue_read_amt: ?u32, - ) ReadResult { - const available_to_read = this.getAvailableToRead( - if (kqueue_read_amt != null) - @as(i64, @intCast(kqueue_read_amt.?)) - else - std.math.maxInt(i64), - ); + this.pending_value.clear(); + this.pending_view = &.{}; + this.pending.run(); + return !was_done; + } - if (this.cannotRead(available_to_read orelse std.math.maxInt(u32))) |res| { - return switch (res) { - .pending => .{ .pending = {} }, - .done => .{ .done = {} }, - else => unreachable, + if (this.reader.isDone()) { + this.pending.result = .{ + .owned_and_done = bun.ByteList.init(buf), + }; + } else { + this.pending.result = .{ + .owned = bun.ByteList.init(buf), }; } + this.buffered = .{}; + this.pending_value.clear(); + this.pending_view = &.{}; + this.pending.run(); + return !was_done; + } else if (!bun.isSliceInBuffer(buf, this.buffered.allocatedSlice())) { + this.buffered.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory(); + } - var buf = buf_; - std.debug.assert(buf.len > 0); + // For pipes, we have to keep pulling or the other process will block. + return this.read_inside_on_pull != .temporary and !(this.buffered.items.len + this.reader.buffer().items.len >= this.highwater_mark and !this.reader.flags.pollable); + } - if (available_to_read) |amt| { - if (amt >= buf.len) { - if (comptime Environment.isLinux) { - this.adjustPipeCapacityOnLinux(amt, buf.len); - } + fn isPulling(this: *const FileReader) bool { + return this.read_inside_on_pull != .none; + } - if (this.auto_sizer) |sizer| { - buf = sizer.resize(amt) catch buf_; - } + pub fn onPull(this: *FileReader, buffer: []u8, array: JSC.JSValue) StreamResult { + array.ensureStillAlive(); + defer array.ensureStillAlive(); + const drained = this.drain(); + + if (drained.len > 0) { + log("onPull({d}) = {d}", .{ buffer.len, drained.len }); + + this.pending_value.clear(); + this.pending_view = &.{}; + + if (buffer.len >= @as(usize, drained.len)) { + @memcpy(buffer[0..drained.len], drained.slice()); + this.buffered.clearAndFree(bun.default_allocator); + + if (this.reader.isDone()) { + return .{ .into_array_and_done = .{ .value = array, .len = drained.len } }; + } else { + return .{ .into_array = .{ .value = array, .len = drained.len } }; } } - return this.doRead(buf); + if (this.reader.isDone()) { + return .{ .owned_and_done = drained }; + } else { + return .{ .owned = drained }; + } } - fn doRead( - this: *@This(), - buf: []u8, - ) ReadResult { - switch (Syscall.read(this.fd, buf)) { - .err => |err| { - const retry = E.AGAIN; - const errno: E = brk: { - const _errno = err.getErrno(); - - if (comptime Environment.isLinux) { - if (_errno == .PERM) - // EPERM and its a FIFO on Linux? Trying to read past a FIFO which has already - // sent a 0 - // Let's retry later. - return .{ .pending = {} }; - } + if (this.reader.isDone()) { + return .{ .done = {} }; + } - break :brk _errno; - }; + if (!this.reader.hasPendingRead()) { + this.read_inside_on_pull = .{ .js = buffer }; + this.reader.read(); - switch (errno) { - retry => { - return .{ .pending = {} }; - }, - else => {}, + defer this.read_inside_on_pull = .{ .none = {} }; + switch (this.read_inside_on_pull) { + .js => |remaining_buf| { + const amount_read = buffer.len - remaining_buf.len; + + log("onPull({d}) = {d}", .{ buffer.len, amount_read }); + + if (amount_read > 0) { + if (this.reader.isDone()) { + return .{ .into_array_and_done = .{ .value = array, .len = @truncate(amount_read) } }; + } + + return .{ .into_array = .{ .value = array, .len = @truncate(amount_read) } }; } - return .{ .err = err }; + if (this.reader.isDone()) { + return .{ .done = {} }; + } }, - .result => |result| { - if (this.poll_ref) |poll| { - if (comptime Environment.isLinux) { - // do not insert .eof here - if (result < buf.len) - poll.flags.remove(.readable); - } else { - // Since we have no way of querying FIFO capacity - // its only okay to read when kqueue says its readable - // otherwise we might block the process - poll.flags.remove(.readable); - } + .temporary => |buf| { + log("onPull({d}) = {d}", .{ buffer.len, buf.len }); + if (this.reader.isDone()) { + return .{ .temporary_and_done = bun.ByteList.init(buf) }; } - if (result == 0) { - return .{ .read = buf[0..0] }; + return .{ .temporary = bun.ByteList.init(buf) }; + }, + .use_buffered => { + const buffered = this.buffered; + this.buffered = .{}; + log("onPull({d}) = {d}", .{ buffer.len, buffered.items.len }); + if (this.reader.isDone()) { + return .{ .owned_and_done = bun.ByteList.init(buffered.items) }; } - return .{ .read = buf[0..result] }; + + return .{ .owned = bun.ByteList.init(buffered.items) }; }, + else => {}, + } + + if (this.reader.isDone()) { + log("onPull({d}) = done", .{buffer.len}); + + return .{ .done = {} }; } } - }; -} -pub const File = struct { - buf: []u8 = &[_]u8{}, - view: JSC.Strong = .{}, + this.pending_value.set(this.parent().globalThis, array); + this.pending_view = buffer; - poll_ref: Async.KeepAlive = .{}, - fd: bun.FileDescriptor = bun.invalid_fd, - concurrent: Concurrent = .{}, - loop: *JSC.EventLoop, - seekable: bool = false, - auto_close: bool = false, - remaining_bytes: Blob.SizeType = std.math.maxInt(Blob.SizeType), - user_chunk_size: Blob.SizeType = 0, - total_read: Blob.SizeType = 0, - mode: bun.Mode = 0, - pending: StreamResult.Pending = .{}, - scheduled_count: u32 = 0, + log("onPull({d}) = pending", .{buffer.len}); + + return .{ .pending = &this.pending }; + } + + pub fn drain(this: *FileReader) bun.ByteList { + if (this.buffered.items.len > 0) { + const out = bun.ByteList.init(this.buffered.items); + this.buffered = .{}; + return out; + } + + if (this.reader.hasPendingRead()) { + return .{}; + } + + const out = this.reader.buffer(); + this.reader.buffer().* = std.ArrayList(u8).init(bun.default_allocator); + return bun.ByteList.fromList(out); + } + + pub fn setRefOrUnref(this: *FileReader, enable: bool) void { + if (this.done) return; + this.has_js_called_unref = !enable; + this.reader.updateRef(enable); + } - pub fn close(this: *File) void { - if (this.auto_close) { - this.auto_close = false; - const fd = this.fd; - if (fd != bun.invalid_fd) { - this.fd = bun.invalid_fd; - _ = Syscall.close(fd); + fn consumeReaderBuffer(this: *FileReader) void { + if (this.buffered.capacity == 0) { + this.buffered = this.reader.buffer().moveToUnmanaged(); + } + } + + pub fn onReaderDone(this: *FileReader) void { + log("onReaderDone()", .{}); + if (!this.isPulling()) { + this.consumeReaderBuffer(); + if (this.pending.state == .pending) { + if (this.buffered.items.len > 0) { + this.pending.result = .{ .owned_and_done = bun.ByteList.fromList(this.buffered) }; + } else { + this.pending.result = .{ .done = {} }; + } + this.buffered = .{}; + this.pending.run(); + } else if (this.buffered.items.len > 0) { + const this_value = this.parent().this_jsvalue; + const globalThis = this.parent().globalThis; + if (this_value != .zero) { + if (Source.onDrainCallbackGetCached(this_value)) |cb| { + const buffered = this.buffered; + this.buffered = .{}; + this.parent().incrementCount(); + defer _ = this.parent().decrementCount(); + this.eventLoop().js.runCallback( + cb, + globalThis, + .undefined, + &.{ + JSC.ArrayBuffer.fromBytes( + buffered.items, + .Uint8Array, + ).toJS( + globalThis, + null, + ), + }, + ); + } + } } } - this.poll_ref.disable(); + this.parent().onClose(); + if (this.waiting_for_onReaderDone) { + this.waiting_for_onReaderDone = false; + _ = this.parent().decrementCount(); + } + } - this.view.clear(); - this.buf.len = 0; + pub fn onReaderError(this: *FileReader, err: bun.sys.Error) void { + this.consumeReaderBuffer(); - this.pending.result = .{ .done = {} }; + this.pending.result = .{ .err = .{ .Error = err } }; this.pending.run(); } - pub fn deinit(this: *File) void { - this.close(); - } + pub const Source = ReadableStreamSource( + @This(), + "File", + onStart, + onPull, + onCancel, + deinit, + setRefOrUnref, + drain, + ); +}; + +pub const ByteBlobLoader = struct { + offset: Blob.SizeType = 0, + store: ?*Blob.Store = null, + chunk_size: Blob.SizeType = 1024 * 1024 * 2, + remain: Blob.SizeType = 1024 * 1024 * 2, + done: bool = false, + pulled: bool = false, - pub fn isClosed(this: *const File) bool { - return this.fd == bun.invalid_fd; + pub const tag = ReadableStream.Tag.Blob; + + pub fn parent(this: *@This()) *Source { + return @fieldParentPtr(Source, "context", this); } - fn calculateChunkSize(this: *File, available_to_read: usize) usize { - const chunk_size: usize = switch (this.user_chunk_size) { - 0 => if (this.isSeekable()) - default_file_chunk_size - else - default_fifo_chunk_size, - else => |size| size, + pub fn setup( + this: *ByteBlobLoader, + blob: *const Blob, + user_chunk_size: Blob.SizeType, + ) void { + blob.store.?.ref(); + var blobe = blob.*; + blobe.resolveSize(); + this.* = ByteBlobLoader{ + .offset = blobe.offset, + .store = blobe.store.?, + .chunk_size = @min( + if (user_chunk_size > 0) @min(user_chunk_size, blobe.size) else blobe.size, + 1024 * 1024 * 2, + ), + .remain = blobe.size, + .done = false, }; - - return if (available_to_read == std.math.maxInt(usize) and this.remaining_bytes > 0 and this.isSeekable()) - @min(chunk_size, this.remaining_bytes -| this.total_read) - else - @min(chunk_size, available_to_read); } - pub fn start( - this: *File, - file: *Blob.FileStore, - ) StreamStart { - var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - - var fd = if (file.pathlike != .path) - // We will always need to close the file descriptor. - switch (Syscall.dup(file.pathlike.fd)) { - .result => |_fd| if (Environment.isWindows) bun.toLibUVOwnedFD(_fd) else _fd, - .err => |err| { - return .{ .err = err.withFd(file.pathlike.fd) }; - }, - } - else switch (Syscall.open(file.pathlike.path.sliceZ(&file_buf), std.os.O.RDONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC, 0)) { - .result => |_fd| _fd, - .err => |err| { - return .{ .err = err.withPath(file.pathlike.path.slice()) }; - }, - }; + pub fn onStart(this: *ByteBlobLoader) StreamStart { + return .{ .chunk_size = this.chunk_size }; + } - if (comptime Environment.isPosix) { - if ((file.is_atty orelse false) or (fd.int() < 3 and std.os.isatty(fd.cast()))) { - var termios = std.mem.zeroes(std.os.termios); - _ = std.c.tcgetattr(fd.cast(), &termios); - bun.C.cfmakeraw(&termios); - file.is_atty = true; - } - } - - if (file.pathlike != .path and !(file.is_atty orelse false)) { - if (comptime !Environment.isWindows) { - // ensure we have non-blocking IO set - switch (Syscall.fcntl(fd, std.os.F.GETFL, 0)) { - .err => return .{ .err = Syscall.Error.fromCode(E.BADF, .fcntl) }, - .result => |flags| { - // if we do not, clone the descriptor and set non-blocking - // it is important for us to clone it so we don't cause Weird Things to happen - if ((flags & std.os.O.NONBLOCK) == 0) { - fd = switch (Syscall.fcntl(fd, std.os.F.DUPFD, 0)) { - .result => |_fd| bun.toFD(_fd), - .err => |err| return .{ .err = err }, - }; - - switch (Syscall.fcntl(fd, std.os.F.SETFL, flags | std.os.O.NONBLOCK)) { - .err => |err| return .{ .err = err }, - .result => |_| {}, - } - } - }, - } - } + pub fn onPull(this: *ByteBlobLoader, buffer: []u8, array: JSC.JSValue) StreamResult { + array.ensureStillAlive(); + defer array.ensureStillAlive(); + this.pulled = true; + const store = this.store orelse return .{ .done = {} }; + if (this.done) { + return .{ .done = {} }; } - var size: Blob.SizeType = 0; - if (comptime Environment.isPosix) { - const stat: bun.Stat = switch (Syscall.fstat(fd)) { - .result => |result| result, - .err => |err| { - _ = Syscall.close(fd); - return .{ .err = err }; - }, - }; - - if (bun.S.ISDIR(stat.mode)) { - _ = Syscall.close(fd); - return .{ .err = Syscall.Error.fromCode(.ISDIR, .fstat) }; - } - file.mode = @as(bun.Mode, @intCast(stat.mode)); - this.mode = file.mode; + var temporary = store.sharedView(); + temporary = temporary[@min(this.offset, temporary.len)..]; - this.seekable = bun.isRegularFile(stat.mode); - file.seekable = this.seekable; - size = @intCast(stat.size); - } else if (comptime Environment.isWindows) outer: { - // without this check the getEndPos call fails unpredictably - if (bun.windows.GetFileType(fd.cast()) != bun.windows.FILE_TYPE_DISK) { - this.seekable = false; - break :outer; - } - size = @intCast(fd.asFile().getEndPos() catch { - this.seekable = false; - break :outer; - }); - this.seekable = true; - } else { - @compileError("Not Implemented"); + temporary = temporary[0..@min(buffer.len, @min(temporary.len, this.remain))]; + if (temporary.len == 0) { + this.clearStore(); + this.done = true; + return .{ .done = {} }; } - if (this.seekable) { - this.remaining_bytes = size; - file.max_size = this.remaining_bytes; - - if (this.remaining_bytes == 0) { - _ = Syscall.close(fd); + const copied = @as(Blob.SizeType, @intCast(temporary.len)); - return .{ .empty = {} }; - } - } else { - file.max_size = Blob.max_size; + this.remain -|= copied; + this.offset +|= copied; + std.debug.assert(buffer.ptr != temporary.ptr); + @memcpy(buffer[0..temporary.len], temporary); + if (this.remain == 0) { + return .{ .into_array_and_done = .{ .value = array, .len = copied } }; } - this.fd = fd; + return .{ .into_array = .{ .value = array, .len = copied } }; + } - return StreamStart{ .ready = {} }; + pub fn detachStore(this: *ByteBlobLoader) ?*Blob.Store { + if (this.store) |store| { + this.store = null; + this.done = true; + return store; + } + return null; } - pub fn isSeekable(this: File) bool { - return this.seekable; + pub fn onCancel(this: *ByteBlobLoader) void { + this.clearStore(); } - const Concurrent = struct { - read: Blob.SizeType = 0, - task: bun.ThreadPool.Task = .{ .callback = Concurrent.taskCallback }, - chunk_size: Blob.SizeType = 0, - main_thread_task: JSC.AnyTask = .{ .callback = onJSThread, .ctx = null }, - concurrent_task: JSC.ConcurrentTask = .{}, + pub fn deinit(this: *ByteBlobLoader) void { + this.clearStore(); + + this.parent().destroy(); + } - pub fn taskCallback(task: *bun.ThreadPool.Task) void { - runAsync(@fieldParentPtr(File, "concurrent", @fieldParentPtr(Concurrent, "task", task))); + fn clearStore(this: *ByteBlobLoader) void { + if (this.store) |store| { + this.store = null; + store.deref(); } + } - pub fn scheduleRead(this: *File) void { - var remaining = this.buf[this.concurrent.read..]; + pub fn drain(this: *ByteBlobLoader) bun.ByteList { + const store = this.store orelse return .{}; + var temporary = store.sharedView(); + temporary = temporary[this.offset..]; + temporary = temporary[0..@min(16384, @min(temporary.len, this.remain))]; - while (remaining.len > 0) { - const to_read = @min(@as(usize, this.concurrent.chunk_size), remaining.len); - switch (Syscall.read(this.fd, remaining[0..to_read])) { - .err => |err| { - const retry = E.AGAIN; + const cloned = bun.ByteList.init(temporary).listManaged(bun.default_allocator).clone() catch @panic("Out of memory"); + this.offset +|= @as(Blob.SizeType, @truncate(cloned.items.len)); + this.remain -|= @as(Blob.SizeType, @truncate(cloned.items.len)); - switch (err.getErrno()) { - retry => break, - else => {}, - } + return bun.ByteList.fromList(cloned); + } - this.pending.result = .{ .err = .{ .Error = err } }; - scheduleMainThreadTask(this); - return; - }, - .result => |result| { - this.concurrent.read += @as(Blob.SizeType, @intCast(result)); - remaining = remaining[result..]; + pub const Source = ReadableStreamSource( + @This(), + "Blob", + onStart, + onPull, + onCancel, + deinit, + null, + drain, + ); +}; - if (result == 0) { - remaining.len = 0; - break; - } - }, - } - } +pub const PipeFunction = *const fn (ctx: *anyopaque, stream: StreamResult, allocator: std.mem.Allocator) void; - scheduleMainThreadTask(this); - } +pub const PathOrFileDescriptor = union(enum) { + path: ZigString.Slice, + fd: bun.FileDescriptor, - pub fn onJSThread(task_ctx: *anyopaque) void { - var this: *File = bun.cast(*File, task_ctx); - const view = this.view.get().?; - defer this.view.clear(); + pub fn deinit(this: *const PathOrFileDescriptor) void { + if (this.* == .path) this.path.deinit(); + } +}; - if (this.isClosed()) { - this.deinit(); +pub const Pipe = struct { + ctx: ?*anyopaque = null, + onPipe: ?PipeFunction = null, - return; + pub fn New(comptime Type: type, comptime Function: anytype) type { + return struct { + pub fn pipe(self: *anyopaque, stream: StreamResult, allocator: std.mem.Allocator) void { + Function(@as(*Type, @ptrCast(@alignCast(self))), stream, allocator); } - if (this.concurrent.read == 0) { - this.pending.result = .{ .done = {} }; - } else if (view != .zero) { - this.pending.result = .{ - .into_array = .{ - .value = view, - .len = @as(Blob.SizeType, @truncate(this.concurrent.read)), - }, - }; - } else { - this.pending.result = .{ - .owned = bun.ByteList.init(this.buf), + pub fn init(self: *Type) Pipe { + return Pipe{ + .ctx = self, + .onPipe = pipe, }; } - - this.pending.run(); - } - - pub fn scheduleMainThreadTask(this: *File) void { - this.concurrent.main_thread_task.ctx = this; - this.loop.enqueueTaskConcurrent(this.concurrent.concurrent_task.from(&this.concurrent.main_thread_task, .manual_deinit)); - } - - fn runAsync(this: *File) void { - this.concurrent.read = 0; - - Concurrent.scheduleRead(this); - } - }; - - pub fn scheduleAsync( - this: *File, - chunk_size: Blob.SizeType, - globalThis: *JSC.JSGlobalObject, - ) void { - this.scheduled_count += 1; - this.poll_ref.ref(globalThis.bunVM()); - this.concurrent.chunk_size = chunk_size; - JSC.WorkPool.schedule(&this.concurrent.task); + }; } +}; - pub fn read(this: *File, buf: []u8) ReadResult { - if (this.fd == bun.invalid_fd) - return .{ .done = {} }; +pub const ByteStream = struct { + buffer: std.ArrayList(u8) = .{ + .allocator = bun.default_allocator, + .items = &.{}, + .capacity = 0, + }, + has_received_last_chunk: bool = false, + pending: StreamResult.Pending = StreamResult.Pending{ + .result = .{ .done = {} }, + }, + done: bool = false, + pending_buffer: []u8 = &.{}, + pending_value: JSC.Strong = .{}, + offset: usize = 0, + highWaterMark: Blob.SizeType = 0, + pipe: Pipe = .{}, + size_hint: Blob.SizeType = 0, - if (this.seekable and this.remaining_bytes == 0) - return .{ .done = {} }; + pub const tag = ReadableStream.Tag.Bytes; - return this.doRead(buf); + pub fn setup(this: *ByteStream) void { + this.* = .{}; } - pub fn readFromJS(this: *File, buf: []u8, view: JSValue, globalThis: *JSC.JSGlobalObject) StreamResult { - const read_result = this.read(buf); - - switch (read_result) { - .read => |slice| if (slice.len == 0) { - this.close(); - return .{ .done = {} }; - }, - .pending => { - if (this.scheduled_count == 0) { - this.buf = buf; - this.view.set(globalThis, view); - this.scheduleAsync(@as(Blob.SizeType, @truncate(buf.len)), globalThis); - } - return .{ .pending = &this.pending }; - }, - else => {}, + pub fn onStart(this: *@This()) StreamStart { + if (this.has_received_last_chunk and this.buffer.items.len == 0) { + return .{ .empty = {} }; } - return read_result.toStream(&this.pending, buf, view, false); - } - - pub fn doRead(this: *File, buf: []u8) ReadResult { - switch (Syscall.read(this.fd, buf)) { - .err => |err| { - const retry = bun.C.E.AGAIN; - const errno = err.getErrno(); + if (this.has_received_last_chunk) { + return .{ .chunk_size = @min(1024 * 1024 * 2, this.buffer.items.len) }; + } - switch (errno) { - retry => { - return .{ .pending = {} }; - }, - else => { - return .{ .err = err }; - }, - } - }, - .result => |result| { - this.remaining_bytes -|= @as(@TypeOf(this.remaining_bytes), @truncate(result)); + if (this.highWaterMark == 0) { + return .{ .ready = {} }; + } - if (result == 0) { - return .{ .done = {} }; - } + return .{ .chunk_size = @max(this.highWaterMark, std.mem.page_size) }; + } - return .{ .read = buf[0..result] }; - }, - } + pub fn value(this: *@This()) JSValue { + const result = this.pending_value.get() orelse { + return .zero; + }; + this.pending_value.clear(); + return result; } -}; -// macOS default pipe size is page_size, 16k, or 64k. It changes based on how much was written -// Linux default pipe size is 16 pages of memory -const default_fifo_chunk_size = 64 * 1024; -const default_file_chunk_size = 1024 * 1024 * 2; + pub fn isCancelled(this: *const @This()) bool { + return @fieldParentPtr(Source, "context", this).cancelled; + } -/// **Not** the Web "FileReader" API -pub const FileReader = struct { - buffered_data: bun.ByteList = .{}, + pub fn unpipeWithoutDeref(this: *@This()) void { + this.pipe.ctx = null; + this.pipe.onPipe = null; + } - total_read: Blob.SizeType = 0, - max_read: Blob.SizeType = 0, + pub fn onData( + this: *@This(), + stream: StreamResult, + allocator: std.mem.Allocator, + ) void { + JSC.markBinding(@src()); + if (this.done) { + if (stream.isDone() and (stream == .owned or stream == .owned_and_done)) { + if (stream == .owned) allocator.free(stream.owned.slice()); + if (stream == .owned_and_done) allocator.free(stream.owned_and_done.slice()); + } - cancelled: bool = false, - started: bool = false, - stored_global_this_: ?*JSC.JSGlobalObject = null, - user_chunk_size: Blob.SizeType = 0, - lazy_readable: Readable.Lazy = undefined, + return; + } - pub fn parent(this: *@This()) *Source { - return @fieldParentPtr(Source, "context", this); - } + std.debug.assert(!this.has_received_last_chunk); + this.has_received_last_chunk = stream.isDone(); - pub fn setSignal(this: *FileReader, signal: Signal) void { - switch (this.lazy_readable) { - .readable => { - if (this.lazy_readable.readable == .FIFO) - this.lazy_readable.readable.FIFO.signal = signal; - }, - else => {}, + if (this.pipe.ctx) |ctx| { + this.pipe.onPipe.?(ctx, stream, allocator); + return; } - } - pub fn readable(this: *FileReader) *Readable { - return &this.lazy_readable.readable; - } + const chunk = stream.slice(); - pub const Readable = union(enum) { - FIFO: FIFO, - File: File, + if (this.pending.state == .pending) { + std.debug.assert(this.buffer.items.len == 0); + const to_copy = this.pending_buffer[0..@min(chunk.len, this.pending_buffer.len)]; + const pending_buffer_len = this.pending_buffer.len; + std.debug.assert(to_copy.ptr != chunk.ptr); + @memcpy(to_copy, chunk[0..to_copy.len]); + this.pending_buffer = &.{}; - pub const Lazy = union(enum) { - readable: Readable, - blob: *Blob.Store, - empty: void, + const is_really_done = this.has_received_last_chunk and to_copy.len <= pending_buffer_len; - pub fn onDrain(this: *Lazy) void { - if (this.* == .readable) { - if (this.readable == .FIFO) { - this.readable.FIFO.drained = true; - } - } - } + if (is_really_done) { + this.done = true; - pub fn finish(this: *Lazy) void { - switch (this.readable) { - .FIFO => { - this.readable.FIFO.finish(); - }, - .File => {}, + if (to_copy.len == 0) { + if (stream == .err) { + if (stream.err == .Error) { + this.pending.result = .{ .err = .{ .Error = stream.err.Error } }; + } + const js_err = stream.err.JSValue; + js_err.ensureStillAlive(); + js_err.protect(); + this.pending.result = .{ .err = .{ .JSValue = js_err } }; + } else { + this.pending.result = .{ + .done = {}, + }; + } + } else { + this.pending.result = .{ + .into_array_and_done = .{ + .value = this.value(), + .len = @as(Blob.SizeType, @truncate(to_copy.len)), + }, + }; } - } - - pub fn isClosed(this: *Lazy) bool { - switch (this.*) { - .empty, .blob => { - return true; - }, - .readable => { - return this.readable.isClosed(); + } else { + this.pending.result = .{ + .into_array = .{ + .value = this.value(), + .len = @as(Blob.SizeType, @truncate(to_copy.len)), }, - } + }; } - pub fn deinit(this: *Lazy) void { - switch (this.*) { - .blob => |blob| { - blob.deref(); - }, - .readable => { - this.readable.deinit(); - }, - .empty => {}, - } - this.* = .{ .empty = {} }; - } - }; + const remaining = chunk[to_copy.len..]; + if (remaining.len > 0) + this.append(stream, to_copy.len, allocator) catch @panic("Out of memory while copying request body"); - pub fn toBlob(this: *Readable) Blob { - if (this.isClosed()) return Blob.initEmpty(JSC.VirtualMachine.get().global); + this.pending.run(); + return; } - pub fn deinit(this: *Readable) void { - switch (this.*) { - .FIFO => { - this.FIFO.close(); - }, - .File => { - this.File.deinit(); - }, - } - } + this.append(stream, 0, allocator) catch @panic("Out of memory while copying request body"); + } + + pub fn append( + this: *@This(), + stream: StreamResult, + offset: usize, + allocator: std.mem.Allocator, + ) !void { + const chunk = stream.slice()[offset..]; - pub fn isClosed(this: *Readable) bool { - switch (this.*) { - .FIFO => { - return this.FIFO.isClosed(); + if (this.buffer.capacity == 0) { + switch (stream) { + .owned => |owned| { + this.buffer = owned.listManaged(allocator); + this.offset += offset; }, - .File => { - return this.File.isClosed(); + .owned_and_done => |owned| { + this.buffer = owned.listManaged(allocator); + this.offset += offset; }, - } - } - - pub fn close(this: *Readable) void { - switch (this.*) { - .FIFO => { - this.FIFO.close(); + .temporary_and_done, .temporary => { + this.buffer = try std.ArrayList(u8).initCapacity(bun.default_allocator, chunk.len); + this.buffer.appendSliceAssumeCapacity(chunk); }, - .File => { - if (this.File.concurrent) |concurrent| { - this.File.concurrent = null; - concurrent.close(); - } - - this.File.close(); + .err => { + this.pending.result = .{ .err = stream.err }; }, + else => unreachable, } + return; } - pub fn read( - this: *Readable, - read_buf: []u8, - view: JSC.JSValue, - global: *JSC.JSGlobalObject, - ) StreamResult { - return switch (std.meta.activeTag(this.*)) { - .FIFO => this.FIFO.readFromJS(read_buf, view, global), - .File => this.File.readFromJS(read_buf, view, global), - }; - } - - pub fn isSeekable(this: Readable) bool { - if (this == .File) { - return this.File.isSeekable(); - } - - return false; - } - - pub fn watch(this: *Readable) void { - switch (this.*) { - .FIFO => { - if (!this.FIFO.isWatching()) - this.FIFO.watch(this.FIFO.fd); - }, - } + switch (stream) { + .temporary_and_done, .temporary => { + try this.buffer.appendSlice(chunk); + }, + .err => { + this.pending.result = .{ .err = stream.err }; + }, + // We don't support the rest of these yet + else => unreachable, } - }; - - pub inline fn globalThis(this: *FileReader) *JSC.JSGlobalObject { - return this.stored_global_this_ orelse @fieldParentPtr(Source, "context", this).globalThis; } - const run_on_different_thread_size = bun.huge_allocator_threshold; - - pub const tag = ReadableStream.Tag.File; - - pub fn fromReadable(this: *FileReader, chunk_size: Blob.SizeType, readable_: *Readable) void { - this.* = .{ - .lazy_readable = .{ - .readable = readable_.*, - }, - }; - this.user_chunk_size = chunk_size; + pub fn setValue(this: *@This(), view: JSC.JSValue) void { + JSC.markBinding(@src()); + this.pending_value.set(this.parent().globalThis, view); } - pub fn finish(this: *FileReader) void { - this.lazy_readable.finish(); + pub fn parent(this: *@This()) *Source { + return @fieldParentPtr(Source, "context", this); } - pub fn onStart(this: *FileReader) StreamStart { - if (!this.started) { - this.started = true; - - switch (this.lazy_readable) { - .blob => |blob| { - defer blob.deref(); - var readable_file = File{ .loop = this.globalThis().bunVM().eventLoop() }; - - const result = readable_file.start(&blob.data.file); - if (result == .empty) { - this.lazy_readable = .{ .empty = {} }; - return result; - } - if (result != .ready) { - return result; - } - - const is_fifo = bun.S.ISFIFO(readable_file.mode) or bun.S.ISCHR(readable_file.mode); + pub fn onPull(this: *@This(), buffer: []u8, view: JSC.JSValue) StreamResult { + JSC.markBinding(@src()); + std.debug.assert(buffer.len > 0); - // for our purposes, ISCHR and ISFIFO are the same - if (is_fifo) { - this.lazy_readable = .{ - .readable = .{ - .FIFO = .{ - .fd = readable_file.fd, - .drained = this.buffered_data.len == 0, - }, - }, - }; - this.lazy_readable.readable.FIFO.watch(readable_file.fd); - this.lazy_readable.readable.FIFO.pollRef().enableKeepingProcessAlive(this.globalThis().bunVM()); - if (!(blob.data.file.is_atty orelse false)) { - this.lazy_readable.readable.FIFO.poll_ref.?.flags.insert(.nonblocking); - } - } else { - this.lazy_readable = .{ - .readable = .{ .File = readable_file }, - }; - } - }, - .readable => {}, - .empty => return .{ .empty = {} }, - } - } else if (this.lazy_readable == .empty) - return .{ .empty = {} }; + if (this.buffer.items.len > 0) { + std.debug.assert(this.value() == .zero); + const to_write = @min( + this.buffer.items.len - this.offset, + buffer.len, + ); + const remaining_in_buffer = this.buffer.items[this.offset..][0..to_write]; - if (this.readable().* == .File) { - const chunk_size = this.readable().File.calculateChunkSize(std.math.maxInt(usize)); - return .{ .chunk_size = @as(Blob.SizeType, @truncate(chunk_size)) }; - } + @memcpy(buffer[0..to_write], this.buffer.items[this.offset..][0..to_write]); - return .{ .chunk_size = if (this.user_chunk_size == 0) default_fifo_chunk_size else this.user_chunk_size }; - } + if (this.offset + to_write == this.buffer.items.len) { + this.offset = 0; + this.buffer.items.len = 0; + } else { + this.offset += to_write; + } - pub fn onPullInto(this: *FileReader, buffer: []u8, view: JSC.JSValue) StreamResult { - std.debug.assert(this.started); + if (this.has_received_last_chunk and remaining_in_buffer.len == 0) { + this.buffer.clearAndFree(); + this.done = true; - // this state isn't really supposed to happen - // but we handle it just in-case - if (this.lazy_readable == .empty) { - if (this.buffered_data.len == 0) { - return .{ .done = {} }; + return .{ + .into_array_and_done = .{ + .value = view, + .len = @as(Blob.SizeType, @truncate(to_write)), + }, + }; } - return .{ .owned_and_done = this.drainInternalBuffer() }; + return .{ + .into_array = .{ + .value = view, + .len = @as(Blob.SizeType, @truncate(to_write)), + }, + }; } - return this.readable().read(buffer, view, this.globalThis()); - } - - fn isFIFO(this: *const FileReader) bool { - if (this.lazy_readable == .readable) { - return this.lazy_readable.readable == .FIFO; + if (this.has_received_last_chunk) { + return .{ + .done = {}, + }; } - return false; - } + this.pending_buffer = buffer; + this.setValue(view); - pub fn finalize(this: *FileReader) void { - this.lazy_readable.deinit(); + return .{ + .pending = &this.pending, + }; } - pub fn onCancel(this: *FileReader) void { - this.cancelled = true; - this.deinit(); - } + pub fn onCancel(this: *@This()) void { + JSC.markBinding(@src()); + const view = this.value(); + if (this.buffer.capacity > 0) this.buffer.clearAndFree(); + this.done = true; + this.pending_value.deinit(); - pub fn deinit(this: *FileReader) void { - this.finalize(); - if (this.lazy_readable.isClosed()) { - this.destroy(); + if (view != .zero) { + this.pending_buffer = &.{}; + this.pending.result = .{ .done = {} }; + this.pending.run(); } } - pub fn destroy(this: *FileReader) void { - bun.default_allocator.destroy(this); - } - - pub fn setRefOrUnref(this: *FileReader, value: bool) void { - if (this.lazy_readable == .readable) { - switch (this.lazy_readable.readable) { - .FIFO => { - if (this.lazy_readable.readable.FIFO.poll_ref) |poll| { - if (value) { - poll.ref(this.globalThis().bunVM()); - } else { - poll.unref(this.globalThis().bunVM()); - } - } - }, - .File => { - if (value) - this.lazy_readable.readable.File.poll_ref.ref(JSC.VirtualMachine.get()) - else - this.lazy_readable.readable.File.poll_ref.unref(JSC.VirtualMachine.get()); - }, - } - } - } + pub fn deinit(this: *@This()) void { + JSC.markBinding(@src()); + if (this.buffer.capacity > 0) this.buffer.clearAndFree(); - pub const setRef = setRefOrUnref; + this.pending_value.deinit(); + if (!this.done) { + this.done = true; - pub fn drainInternalBuffer(this: *FileReader) bun.ByteList { - const buffered = this.buffered_data; - this.lazy_readable.onDrain(); - if (buffered.cap > 0) { - this.buffered_data = .{}; + this.pending_buffer = &.{}; + this.pending.result = .{ .done = {} }; + this.pending.run(); } - return buffered; + this.parent().destroy(); } pub const Source = ReadableStreamSource( @This(), - "FileReader", + "Bytes", onStart, - onPullInto, + onPull, onCancel, deinit, - setRefOrUnref, - drainInternalBuffer, + null, + null, ); }; +pub const ReadResult = union(enum) { + pending: void, + err: Syscall.Error, + done: void, + read: []u8, + + pub fn toStream(this: ReadResult, pending: *StreamResult.Pending, buf: []u8, view: JSValue, close_on_empty: bool) StreamResult { + return toStreamWithIsDone( + this, + pending, + buf, + view, + close_on_empty, + false, + ); + } + pub fn toStreamWithIsDone(this: ReadResult, pending: *StreamResult.Pending, buf: []u8, view: JSValue, close_on_empty: bool, is_done: bool) StreamResult { + return switch (this) { + .pending => .{ .pending = pending }, + .err => .{ .err = .{ .Error = this.err } }, + .done => .{ .done = {} }, + .read => |slice| brk: { + const owned = slice.ptr != buf.ptr; + const done = is_done or (close_on_empty and slice.len == 0); + + break :brk if (owned and done) + StreamResult{ .owned_and_done = bun.ByteList.init(slice) } + else if (owned) + StreamResult{ .owned = bun.ByteList.init(slice) } + else if (done) + StreamResult{ .into_array_and_done = .{ .len = @as(Blob.SizeType, @truncate(slice.len)), .value = view } } + else + StreamResult{ .into_array = .{ .len = @as(Blob.SizeType, @truncate(slice.len)), .value = view } }; + }, + }; + } +}; + +pub const AutoSizer = struct { + buffer: *bun.ByteList, + allocator: std.mem.Allocator, + max: usize, + + pub fn resize(this: *AutoSizer, size: usize) ![]u8 { + const available = this.buffer.cap - this.buffer.len; + if (available >= size) return this.buffer.ptr[this.buffer.len..this.buffer.cap][0..size]; + const to_grow = size -| available; + if (to_grow + @as(usize, this.buffer.cap) > this.max) + return this.buffer.ptr[this.buffer.len..this.buffer.cap]; + + var list = this.buffer.listManaged(this.allocator); + const prev_len = list.items.len; + try list.ensureTotalCapacity(to_grow + @as(usize, this.buffer.cap)); + this.buffer.update(list); + return this.buffer.ptr[prev_len..@as(usize, this.buffer.cap)]; + } +}; + +// Linux default pipe size is 16 pages of memory +const default_fifo_chunk_size = 64 * 1024; +const default_file_chunk_size = 1024 * 1024 * 2; + pub fn NewReadyWatcher( comptime Context: type, comptime flag_: Async.FilePoll.Flags, @@ -5221,7 +4458,6 @@ pub fn NewReadyWatcher( } }; } - // pub const HTTPRequest = RequestBodyStreamer(false); // pub const HTTPSRequest = RequestBodyStreamer(true); // pub fn ResponseBodyStreamer(comptime is_ssl: bool) type { diff --git a/src/bun.zig b/src/bun.zig index 43d8f3b175fed1..45a88d7a487a83 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -48,10 +48,10 @@ pub const allocators = @import("./allocators.zig"); pub const shell = struct { pub usingnamespace @import("./shell/shell.zig"); + pub const ShellSubprocess = @import("./shell/subproc.zig").ShellSubprocess; + // pub const ShellSubprocessMini = @import("./shell/subproc.zig").ShellSubprocessMini; }; -pub const ShellSubprocess = @import("./shell/subproc.zig").ShellSubprocess; - pub const Output = @import("./output.zig"); pub const Global = @import("./__global.zig"); @@ -482,28 +482,34 @@ pub fn ensureNonBlocking(fd: anytype) void { _ = std.os.fcntl(fd, std.os.F.SETFL, current | std.os.O.NONBLOCK) catch 0; } -const global_scope_log = Output.scoped(.bun, false); +const global_scope_log = sys.syslog; pub fn isReadable(fd: FileDescriptor) PollFlag { if (comptime Environment.isWindows) { @panic("TODO on Windows"); } - + std.debug.assert(fd != invalid_fd); var polls = [_]std.os.pollfd{ .{ .fd = fd.cast(), - .events = std.os.POLL.IN | std.os.POLL.ERR, + .events = std.os.POLL.IN | std.os.POLL.ERR | std.os.POLL.HUP, .revents = 0, }, }; const result = (std.os.poll(&polls, 0) catch 0) != 0; - global_scope_log("poll({}) readable: {any} ({d})", .{ fd, result, polls[0].revents }); - return if (result and polls[0].revents & std.os.POLL.HUP != 0) + const rc = if (result and polls[0].revents & (std.os.POLL.HUP | std.os.POLL.ERR) != 0) PollFlag.hup else if (result) PollFlag.ready else PollFlag.not_ready; + global_scope_log("poll({}, .readable): {any} ({s}{s})", .{ + fd, + result, + @tagName(rc), + if (polls[0].revents & std.os.POLL.ERR != 0) " ERR " else "", + }); + return rc; } pub const PollFlag = enum { ready, not_ready, hup }; @@ -528,24 +534,30 @@ pub fn isWritable(fd: FileDescriptor) PollFlag { } return; } + std.debug.assert(fd != invalid_fd); var polls = [_]std.os.pollfd{ .{ .fd = fd.cast(), - .events = std.os.POLL.OUT, + .events = std.os.POLL.OUT | std.os.POLL.ERR | std.os.POLL.HUP, .revents = 0, }, }; const result = (std.os.poll(&polls, 0) catch 0) != 0; - global_scope_log("poll({}) writable: {any} ({d})", .{ fd, result, polls[0].revents }); - if (result and polls[0].revents & std.os.POLL.HUP != 0) { - return .hup; - } else if (result) { - return .ready; - } else { - return .not_ready; - } + const rc = if (result and polls[0].revents & (std.os.POLL.HUP | std.os.POLL.ERR) != 0) + PollFlag.hup + else if (result) + PollFlag.ready + else + PollFlag.not_ready; + global_scope_log("poll({}, .writable): {any} ({s}{s})", .{ + fd, + result, + @tagName(rc), + if (polls[0].revents & std.os.POLL.ERR != 0) " ERR " else "", + }); + return rc; } /// Do not use this function, call std.debug.panic directly. @@ -601,6 +613,7 @@ pub fn isHeapMemory(memory: anytype) bool { pub const Mimalloc = @import("./allocators/mimalloc.zig"); pub const isSliceInBuffer = allocators.isSliceInBuffer; +pub const isSliceInBufferT = allocators.isSliceInBufferT; pub inline fn sliceInBuffer(stable: string, value: string) string { if (allocators.sliceRange(stable, value)) |_| { @@ -613,7 +626,7 @@ pub inline fn sliceInBuffer(stable: string, value: string) string { } pub fn rangeOfSliceInBuffer(slice: []const u8, buffer: []const u8) ?[2]u32 { - if (!isSliceInBuffer(u8, slice, buffer)) return null; + if (!isSliceInBuffer(slice, buffer)) return null; const r = [_]u32{ @as(u32, @truncate(@intFromPtr(slice.ptr) -| @intFromPtr(buffer.ptr))), @as(u32, @truncate(slice.len)), @@ -716,7 +729,7 @@ pub fn getenvZ(path_: [:0]const u8) ?[]const u8 { const line = sliceTo(lineZ, 0); const key_end = strings.indexOfCharUsize(line, '=') orelse line.len; const key = line[0..key_end]; - if (strings.eqlLong(key, path_, true)) { + if (strings.eqlInsensitive(key, path_)) { return line[@min(key_end + 1, line.len)..]; } } @@ -993,6 +1006,10 @@ pub const SignalCode = enum(u8) { SIGSYS = 31, _, + // The `subprocess.kill()` method sends a signal to the child process. If no + // argument is given, the process will be sent the 'SIGTERM' signal. + pub const default = @intFromEnum(SignalCode.SIGTERM); + pub const Map = ComptimeEnumMap(SignalCode); pub fn name(value: SignalCode) ?[]const u8 { if (@intFromEnum(value) <= @intFromEnum(SignalCode.SIGSYS)) { return asByteSlice(@tagName(value)); @@ -1001,6 +1018,15 @@ pub const SignalCode = enum(u8) { return null; } + /// Shell scripts use exit codes 128 + signal number + /// https://tldp.org/LDP/abs/html/exitcodes.html + pub fn toExitCode(value: SignalCode) ?u8 { + return switch (@intFromEnum(value)) { + 1...31 => 128 +% @intFromEnum(value), + else => null, + }; + } + pub fn description(signal: SignalCode) ?[]const u8 { // Description names copied from fish // https://github.com/fish-shell/fish-shell/blob/00ffc397b493f67e28f18640d3de808af29b1434/fish-rust/src/signal.rs#L420 @@ -2000,6 +2026,8 @@ pub const win32 = struct { }; } + pub const spawn = @import("./bun.js/api/bun/spawn.zig").PosixSpawn; + pub fn isWatcherChild() bool { var buf: [1]u16 = undefined; return windows.GetEnvironmentVariableW(@constCast(watcherChildEnv.ptr), &buf, 1) > 0; @@ -2528,6 +2556,10 @@ pub fn NewRefCounted(comptime T: type, comptime deinit_fn: ?fn (self: *T) void) } } + const output_name: []const u8 = if (@hasDecl(T, "DEBUG_REFCOUNT_NAME")) T.DEBUG_REFCOUNT_NAME else meta.typeBaseName(@typeName(T)); + + const log = Output.scoped(output_name, true); + return struct { const allocation_logger = Output.scoped(.alloc, @hasDecl(T, "logAllocations")); @@ -2545,10 +2577,12 @@ pub fn NewRefCounted(comptime T: type, comptime deinit_fn: ?fn (self: *T) void) } pub fn ref(self: *T) void { + log("0x{x} ref {d} + 1 = {d}", .{ @intFromPtr(self), self.ref_count, self.ref_count + 1 }); self.ref_count += 1; } pub fn deref(self: *T) void { + log("0x{x} deref {d} - 1 = {d}", .{ @intFromPtr(self), self.ref_count, self.ref_count - 1 }); self.ref_count -= 1; if (self.ref_count == 0) { @@ -2566,7 +2600,9 @@ pub fn NewRefCounted(comptime T: type, comptime deinit_fn: ?fn (self: *T) void) ptr.* = t; if (comptime Environment.allow_assert) { - std.debug.assert(ptr.ref_count == 1); + if (ptr.ref_count != 1) { + std.debug.panic("Expected ref_count to be 1, got {d}", .{ptr.ref_count}); + } allocation_logger("new() = {*}", .{ptr}); } @@ -2710,11 +2746,34 @@ pub fn getUserName(output_buffer: []u8) ?[]const u8 { return output_buffer[0..size]; } -/// This struct is a workaround a Windows terminal bug. -/// TODO: when https://github.com/microsoft/terminal/issues/16606 is resolved, revert this commit. -pub var buffered_stdin = std.io.BufferedReader(4096, std.fs.File.Reader){ - .unbuffered_reader = std.fs.File.Reader{ .context = .{ .handle = if (Environment.isWindows) undefined else 0 } }, -}; +pub inline fn markWindowsOnly() if (Environment.isWindows) void else noreturn { + if (Environment.isWindows) { + return; + } + + if (@inComptime()) { + @compileError("This function is only available on Windows"); + } + + @panic("Assertion failure: this function should only be accessible on Windows."); +} + +pub inline fn markPosixOnly() if (Environment.isPosix) void else noreturn { + if (Environment.isPosix) { + return; + } + + if (@inComptime()) { + @compileError("This function is only available on POSIX"); + } + + @panic("Assertion failure: this function should only be accessible on POSIX."); +} + +pub fn linuxKernelVersion() Semver.Version { + if (comptime !Environment.isLinux) @compileError("linuxKernelVersion() is only available on Linux"); + return @import("./analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion(); +} pub const WindowsSpawnWorkaround = @import("./child_process_windows.zig"); diff --git a/src/bun_js.zig b/src/bun_js.zig index d855002451743d..b96de874897062 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -136,7 +136,7 @@ pub const Run = struct { try bundle.runEnvLoader(); const mini = JSC.MiniEventLoop.initGlobal(bundle.env); mini.top_level_dir = ctx.args.absolute_working_dir orelse ""; - try bun.shell.InterpreterMini.initAndRunFromFile(mini, entry_path); + try bun.shell.Interpreter.initAndRunFromFile(mini, entry_path); return; } diff --git a/src/bundler.zig b/src/bundler.zig index 4049547dcb397f..8ddeaf7941ff35 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1028,7 +1028,7 @@ pub const Bundler = struct { Output.panic("TODO: dataurl, base64", .{}); // TODO }, .css => { - var file: std.fs.File = undefined; + var file: bun.sys.File = undefined; if (Outstream == std.fs.Dir) { const output_dir = outstream; @@ -1036,9 +1036,9 @@ pub const Bundler = struct { if (std.fs.path.dirname(file_path.pretty)) |dirname| { try output_dir.makePath(dirname); } - file = try output_dir.createFile(file_path.pretty, .{}); + file = bun.sys.File.from(try output_dir.createFile(file_path.pretty, .{})); } else { - file = outstream; + file = bun.sys.File.from(outstream); } const CSSBuildContext = struct { @@ -1046,7 +1046,7 @@ pub const Bundler = struct { }; const build_ctx = CSSBuildContext{ .origin = bundler.options.origin }; - const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, std.fs.File.Writer)); + const BufferedWriter = std.io.CountingWriter(std.io.BufferedWriter(8192, bun.sys.File.Writer)); const CSSWriter = Css.NewWriter( BufferedWriter.Writer, @TypeOf(&bundler.linker), @@ -1844,7 +1844,7 @@ pub const Bundler = struct { const did_start = false; if (bundler.options.output_dir_handle == null) { - const outstream = std.io.getStdOut(); + const outstream = bun.sys.File.from(std.io.getStdOut()); if (!did_start) { try switch (bundler.options.import_path_format) { diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 0ed79fc2402c41..986aa9871858ca 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -460,13 +460,12 @@ pub const BundleV2 = struct { return visitor.reachable.toOwnedSlice(); } - fn isDone(ptr: *anyopaque) bool { - var this = bun.cast(*const BundleV2, ptr); + fn isDone(this: *BundleV2) bool { return @atomicLoad(usize, &this.graph.parse_pending, .Monotonic) == 0 and @atomicLoad(usize, &this.graph.resolve_pending, .Monotonic) == 0; } pub fn waitForParse(this: *BundleV2) void { - this.loop().tick(this, isDone); + this.loop().tick(this, &isDone); debug("Parsed {d} files, producing {d} ASTs", .{ this.graph.input_files.len, this.graph.ast.len }); } @@ -1550,7 +1549,7 @@ pub const BundleV2 = struct { pub fn generateInNewThreadWrap(instance: *BundleThread) void { Output.Source.configureNamedThread("Bundler"); - instance.waker = bun.Async.Waker.init(bun.default_allocator) catch @panic("Failed to create waker"); + instance.waker = bun.Async.Waker.init() catch @panic("Failed to create waker"); var has_bundled = false; while (true) { diff --git a/src/c.zig b/src/c.zig index 3096c4094b575f..a01d6f4ab4567f 100644 --- a/src/c.zig +++ b/src/c.zig @@ -462,3 +462,5 @@ pub fn dlopen(filename: [:0]const u8, flags: i32) ?*anyopaque { return std.c.dlopen(filename, flags); } + +pub extern "C" fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int; diff --git a/src/cli.zig b/src/cli.zig index 94d572de5932cc..0699666d57da40 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -55,17 +55,13 @@ pub const Cli = struct { var panicker = MainPanicHandler.init(log); MainPanicHandler.Singleton = &panicker; Command.start(allocator, log) catch |err| { - switch (err) { - else => { - if (Output.enable_ansi_colors_stderr) { - log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; - } else { - log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; - } + log.printForLogLevel(Output.errorWriter()) catch {}; - Reporter.globalError(err, @errorReturnTrace()); - }, + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); } + + Reporter.globalError(err, null); }; } @@ -198,9 +194,10 @@ pub const Arguments = struct { 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 .{}; + clap.parseParam("--system-shell Use cmd.exe to interpret package.json scripts") catch unreachable, + } else .{ + clap.parseParam("--bun-shell Use Bun Shell to interpret package.json scripts") catch unreachable, + }; pub const run_params = run_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_; const bunx_commands = [_]ParamType{ @@ -850,10 +847,10 @@ pub const Arguments = struct { ctx.debug.output_file = output_file.?; if (cmd == .RunCommand) { - ctx.debug.use_native_shell = if (Environment.isWindows) - !args.flag("--no-native-shell") + ctx.debug.use_system_shell = if (Environment.isWindows) + args.flag("--system-shell") else - true; + !args.flag("--bun-shell"); } return opts; @@ -979,7 +976,7 @@ pub const HelpCommand = struct { .explicit => { Output.pretty( "Bun is a fast JavaScript runtime, package manager, bundler, and test runner. (" ++ - Global.package_json_version_with_sha ++ + Global.package_json_version_with_revision ++ ")\n\n" ++ cli_helptext_fmt, args, @@ -1053,7 +1050,7 @@ pub const Command = struct { 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, + use_system_shell: bool = false, // technical debt macros: MacroOptions = MacroOptions.unspecified, @@ -1550,33 +1547,32 @@ pub const Command = struct { return; } - // iterate over args - // if --help, print help and exit - const print_help = brk: { - for (bun.argv()) |arg| { - if (strings.eqlComptime(arg, "--help") or strings.eqlComptime(arg, "-h")) { - break :brk true; - } - } - break :brk false; - }; - var template_name_start: usize = 0; var positionals: [2]string = .{ "", "" }; - var positional_i: usize = 0; + var dash_dash_bun = false; + var print_help = false; if (args.len > 2) { - const remainder = args[2..]; + const remainder = args[1..]; var remainder_i: usize = 0; while (remainder_i < remainder.len and positional_i < positionals.len) : (remainder_i += 1) { - const slice = std.mem.trim(u8, bun.asByteSlice(remainder[remainder_i]), " \t\n;"); - if (slice.len > 0 and !strings.hasPrefixComptime(slice, "--")) { - if (positional_i == 0) { - template_name_start = remainder_i + 2; + const slice = std.mem.trim(u8, bun.asByteSlice(remainder[remainder_i]), " \t\n"); + if (slice.len > 0) { + if (!strings.hasPrefixComptime(slice, "--")) { + if (positional_i == 1) { + template_name_start = remainder_i + 2; + } + positionals[positional_i] = slice; + positional_i += 1; + } + if (slice[0] == '-') { + if (strings.eqlComptime(slice, "--bun")) { + dash_dash_bun = true; + } else if (strings.eqlComptime(slice, "--help") or strings.eqlComptime(slice, "-h")) { + print_help = true; + } } - positionals[positional_i] = slice; - positional_i += 1; } } } @@ -1585,14 +1581,14 @@ pub const Command = struct { // "bun create --" // "bun create -abc --" positional_i == 0 or - positionals[0].len == 0) + positionals[1].len == 0) { Command.Tag.printHelp(.CreateCommand, true); Global.exit(0); return; } - const template_name = positionals[0]; + const template_name = positionals[1]; // if template_name is "react" // print message telling user to use "bun create vite" instead @@ -1638,10 +1634,13 @@ pub const Command = struct { example_tag != CreateCommandExample.Tag.local_folder; if (use_bunx) { - const bunx_args = try allocator.alloc([:0]const u8, 1 + args.len - template_name_start); + const bunx_args = try allocator.alloc([:0]const u8, 2 + args.len - template_name_start + @intFromBool(dash_dash_bun)); bunx_args[0] = "bunx"; - bunx_args[1] = try BunxCommand.addCreatePrefix(allocator, template_name); - for (bunx_args[2..], args[template_name_start + 1 ..]) |*dest, src| { + if (dash_dash_bun) { + bunx_args[1] = "--bun"; + } + bunx_args[1 + @as(usize, @intFromBool(dash_dash_bun))] = try BunxCommand.addCreatePrefix(allocator, template_name); + for (bunx_args[2 + @as(usize, @intFromBool(dash_dash_bun)) ..], args[template_name_start..]) |*dest, src| { dest.* = src; } @@ -1740,7 +1739,7 @@ pub const Command = struct { } if (extension.len > 0) { - if (strings.endsWithComptime(ctx.args.entry_points[0], ".bun.sh")) { + if (strings.endsWithComptime(ctx.args.entry_points[0], ".sh")) { break :brk options.Loader.bunsh; } diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 05153b35dc2e7c..2af41fc381d185 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -39,7 +39,7 @@ pub const InitCommand = struct { Output.flush(); - var input = try bun.buffered_stdin.reader().readUntilDelimiterAlloc(alloc, '\n', 1024); + var input = try bun.Output.buffered_stdin.reader().readUntilDelimiterAlloc(alloc, '\n', 1024); if (strings.endsWithChar(input, '\r')) { input = input[0 .. input.len - 1]; } diff --git a/src/cli/package_manager_command.zig b/src/cli/package_manager_command.zig index 5cfb09ce91aadb..a7d3939a9313f4 100644 --- a/src/cli/package_manager_command.zig +++ b/src/cli/package_manager_command.zig @@ -553,8 +553,9 @@ pub const PackageManagerCommand = struct { } } + const loop = pm.event_loop.loop(); while (pm.pending_lifecycle_script_tasks.load(.Monotonic) > 0) { - pm.uws_event_loop.tick(); + loop.tick(); } } } diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 925b60da619bfb..097d41a239fee1 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -273,7 +273,7 @@ pub const RunCommand = struct { env: *DotEnv.Loader, passthrough: []const string, silent: bool, - use_native_shell: bool, + use_system_shell: bool, ) !bool { const shell_bin = findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell; @@ -306,7 +306,7 @@ pub const RunCommand = struct { combined_script = combined_script_buf; } - if (Environment.isWindows and !use_native_shell) { + if (!use_system_shell) { if (!silent) { if (Environment.isDebug) { Output.prettyError("[bun shell] ", .{}); @@ -316,7 +316,7 @@ pub const RunCommand = struct { } const mini = bun.JSC.MiniEventLoop.initGlobal(env); - bun.shell.InterpreterMini.initAndRunFromSource(mini, name, combined_script) catch |err| { + bun.shell.Interpreter.initAndRunFromSource(mini, name, combined_script) catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } @@ -431,9 +431,11 @@ pub const RunCommand = struct { if (Environment.isWindows and bun.strings.hasSuffixComptime(executable, ".exe")) { std.debug.assert(std.fs.path.isAbsolute(executable)); - // Using @constCast is safe because we know that `direct_launch_buffer` is the data destination + // Using @constCast is safe because we know that + // `direct_launch_buffer` is the data destination that assumption is + // backed by the immediate assertion. var wpath = @constCast(bun.strings.toNTPath(&BunXFastPath.direct_launch_buffer, executable)); - std.debug.assert(bun.isSliceInBuffer(u16, wpath, &BunXFastPath.direct_launch_buffer)); + std.debug.assert(bun.isSliceInBufferT(u16, wpath, &BunXFastPath.direct_launch_buffer)); std.debug.assert(wpath.len > bun.windows.nt_object_prefix.len + ".exe".len); wpath.len += ".bunx".len - ".exe".len; @@ -1372,7 +1374,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, - ctx.debug.use_native_shell, + ctx.debug.use_system_shell, )) { return false; } @@ -1386,7 +1388,7 @@ pub const RunCommand = struct { this_bundler.env, passthrough, ctx.debug.silent, - ctx.debug.use_native_shell, + ctx.debug.use_system_shell, )) return false; temp_script_buffer[0.."post".len].* = "post".*; @@ -1400,7 +1402,7 @@ pub const RunCommand = struct { this_bundler.env, &.{}, ctx.debug.silent, - ctx.debug.use_native_shell, + ctx.debug.use_system_shell, )) { return false; } @@ -1581,7 +1583,7 @@ pub const BunXFastPath = struct { /// If this returns, it implies the fast path cannot be taken fn tryLaunch(ctx: Command.Context, path_to_use: [:0]u16, env: *DotEnv.Loader, passthrough: []const []const u8) void { - std.debug.assert(bun.isSliceInBuffer(u16, path_to_use, &BunXFastPath.direct_launch_buffer)); + std.debug.assert(bun.isSliceInBufferT(u16, path_to_use, &BunXFastPath.direct_launch_buffer)); var command_line = BunXFastPath.direct_launch_buffer[path_to_use.len..]; debug("Attempting to find and load bunx file: '{}'", .{bun.fmt.utf16(path_to_use)}); diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 3a1e45854fa362..5fb6d7a3a8040f 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -168,7 +168,7 @@ pub const CommandLineReporter = struct { } pub fn handleTestPass(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - const writer_: std.fs.File.Writer = Output.errorWriter(); + const writer_ = Output.errorWriter(); var buffered_writer = std.io.bufferedWriter(writer_); var writer = buffered_writer.writer(); defer buffered_writer.flush() catch unreachable; @@ -185,7 +185,7 @@ pub const CommandLineReporter = struct { } pub fn handleTestFail(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var writer_: std.fs.File.Writer = Output.errorWriter(); + var writer_ = Output.errorWriter(); var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb); // when the tests fail, we want to repeat the failures at the end @@ -218,7 +218,7 @@ pub const CommandLineReporter = struct { } pub fn handleTestSkip(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var writer_: std.fs.File.Writer = Output.errorWriter(); + var writer_ = Output.errorWriter(); var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb); // If you do it.only, don't report the skipped tests because its pretty noisy @@ -242,7 +242,8 @@ pub const CommandLineReporter = struct { } pub fn handleTestTodo(cb: *TestRunner.Callback, id: Test.ID, _: string, label: string, expectations: u32, elapsed_ns: u64, parent: ?*jest.DescribeScope) void { - var writer_: std.fs.File.Writer = Output.errorWriter(); + var writer_ = Output.errorWriter(); + var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb); // when the tests skip, we want to repeat the failures at the end diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index ca33a3258af5e2..fa70e43b183f8d 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -1776,9 +1776,9 @@ function generateLazyClassStructureHeader(typeName, { klass = {}, proto = {}, zi if (zigOnly) return ""; return ` - JSC::Structure* ${className(typeName)}Structure() { return m_${className(typeName)}.getInitializedOnMainThread(this); } - JSC::JSObject* ${className(typeName)}Constructor() { return m_${className(typeName)}.constructorInitializedOnMainThread(this); } - JSC::JSValue ${className(typeName)}Prototype() { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } + JSC::Structure* ${className(typeName)}Structure() const { return m_${className(typeName)}.getInitializedOnMainThread(this); } + JSC::JSObject* ${className(typeName)}Constructor() const { return m_${className(typeName)}.constructorInitializedOnMainThread(this); } + JSC::JSValue ${className(typeName)}Prototype() const { return m_${className(typeName)}.prototypeInitializedOnMainThread(this); } JSC::LazyClassStructure m_${className(typeName)}; `.trim(); } diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 3f1f542d196e40..46bfb04093267d 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -1,6 +1,6 @@ import { resolve, join } from "path"; -const classes = ["ArrayBufferSink", "FileSink", "HTTPResponseSink", "HTTPSResponseSink", "UVStreamSink"]; +const classes = ["ArrayBufferSink", "FileSink", "HTTPResponseSink", "HTTPSResponseSink"]; function names(name) { return { @@ -64,7 +64,7 @@ function header() { class ${className} final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; - static ${className}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr); + static ${className}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr, uintptr_t destructor = 0); static constexpr SinkID Sink = SinkID::${name}; DECLARE_EXPORT_INFO; @@ -105,11 +105,14 @@ function header() { void* m_sinkPtr; int m_refCount { 1 }; + + uintptr_t m_onDestroy { 0 }; - ${className}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + ${className}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr, uintptr_t onDestroy) : Base(vm, structure) { m_sinkPtr = sinkPtr; + m_onDestroy = onDestroy; } void finishCreation(JSC::VM&); @@ -120,7 +123,7 @@ function header() { class ${controller} final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; - static ${controller}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr); + static ${controller}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr, uintptr_t onDestroy); static constexpr SinkID Sink = SinkID::${name}; DECLARE_EXPORT_INFO; @@ -158,11 +161,14 @@ function header() { mutable WriteBarrier m_onPull; mutable WriteBarrier m_onClose; mutable JSC::Weak m_weakReadableStream; + + uintptr_t m_onDestroy { 0 }; - ${controller}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + ${controller}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr, uintptr_t onDestroy) : Base(vm, structure) { m_sinkPtr = sinkPtr; + m_onDestroy = onDestroy; } void finishCreation(JSC::VM&); @@ -267,7 +273,7 @@ async function implementation() { #include #include - +extern "C" void Bun__onSinkDestroyed(uintptr_t destructor, void* sinkPtr); namespace WebCore { using namespace JSC; @@ -403,7 +409,6 @@ JSC_DEFINE_CUSTOM_GETTER(function${name}__getter, (JSC::JSGlobalObject * lexical return JSC::JSValue::encode(globalObject->${name}()); } - JSC_DECLARE_HOST_FUNCTION(${controller}__close); JSC_DEFINE_HOST_FUNCTION(${controller}__close, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) { @@ -562,6 +567,10 @@ const ClassInfo ${controller}::s_info = { "${controllerName}"_s, &Base::s_info, ${className}::~${className}() { + if (m_onDestroy) { + Bun__onSinkDestroyed(m_onDestroy, m_sinkPtr); + } + if (m_sinkPtr) { ${name}__finalize(m_sinkPtr); } @@ -570,6 +579,10 @@ ${className}::~${className}() ${controller}::~${controller}() { + if (m_onDestroy) { + Bun__onSinkDestroyed(m_onDestroy, m_sinkPtr); + } + if (m_sinkPtr) { ${name}__finalize(m_sinkPtr); } @@ -586,6 +599,12 @@ JSObject* JS${controllerName}::createPrototype(VM& vm, JSDOMGlobalObject& global } void JS${controllerName}::detach() { + if (m_onDestroy) { + auto destroy = m_onDestroy; + m_onDestroy = 0; + Bun__onSinkDestroyed(destroy, m_sinkPtr); + } + m_sinkPtr = nullptr; m_onPull.clear(); @@ -617,16 +636,16 @@ ${constructor}* ${constructor}::create(JSC::VM& vm, JSC::JSGlobalObject* globalO return ptr; } -${className}* ${className}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr) +${className}* ${className}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr, uintptr_t onDestroy) { - ${className}* ptr = new (NotNull, JSC::allocateCell<${className}>(vm)) ${className}(vm, structure, sinkPtr); + ${className}* ptr = new (NotNull, JSC::allocateCell<${className}>(vm)) ${className}(vm, structure, sinkPtr, onDestroy); ptr->finishCreation(vm); return ptr; } -${controller}* ${controller}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr) +${controller}* ${controller}::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* sinkPtr, uintptr_t onDestroy) { - ${controller}* ptr = new (NotNull, JSC::allocateCell<${controller}>(vm)) ${controller}(vm, structure, sinkPtr); + ${controller}* ptr = new (NotNull, JSC::allocateCell<${controller}>(vm)) ${controller}(vm, structure, sinkPtr, onDestroy); ptr->finishCreation(vm); return ptr; } @@ -679,6 +698,15 @@ void ${controller}::finishCreation(VM& vm) ASSERT(inherits(info())); } +extern "C" void ${name}__setDestroyCallback(EncodedJSValue encodedValue, uintptr_t callback) +{ + JSValue value = JSValue::decode(encodedValue); + if (auto* sink = JSC::jsDynamicCast(value)) { + sink->m_onDestroy = callback; + } else if (auto* controller = JSC::jsDynamicCast(value)) { + controller->m_onDestroy = callback; + } +} void ${className}::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) { @@ -817,12 +845,12 @@ default: const { className, controller, prototypeName, controllerPrototypeName, constructor } = names(name); templ += ` -extern "C" JSC__JSValue ${name}__createObject(JSC__JSGlobalObject* arg0, void* sinkPtr) +extern "C" JSC__JSValue ${name}__createObject(JSC__JSGlobalObject* arg0, void* sinkPtr, uintptr_t destructor) { auto& vm = arg0->vm(); Zig::GlobalObject* globalObject = reinterpret_cast(arg0); JSC::Structure* structure = globalObject->${name}Structure(); - return JSC::JSValue::encode(WebCore::JS${name}::create(vm, globalObject, structure, sinkPtr)); + return JSC::JSValue::encode(WebCore::JS${name}::create(vm, globalObject, structure, sinkPtr, destructor)); } extern "C" void* ${name}__fromJS(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1) @@ -857,7 +885,7 @@ extern "C" JSC__JSValue ${name}__assignToStream(JSC__JSGlobalObject* arg0, JSC__ Zig::GlobalObject* globalObject = reinterpret_cast(arg0); JSC::Structure* structure = WebCore::getDOMStructure(vm, *globalObject); - WebCore::${controller} *controller = WebCore::${controller}::create(vm, globalObject, structure, sinkPtr); + WebCore::${controller} *controller = WebCore::${controller}::create(vm, globalObject, structure, sinkPtr, 0); *controllerValue = reinterpret_cast(JSC::JSValue::encode(controller)); return globalObject->assignToStream(JSC::JSValue::decode(stream), controller); } diff --git a/src/darwin_c.zig b/src/darwin_c.zig index b3ec5b1015f248..e38d8c57227b4e 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -9,7 +9,7 @@ const StatError = std.fs.File.StatError; const off_t = std.c.off_t; const errno = os.errno; const zeroes = mem.zeroes; - +const This = @This(); pub extern "c" fn copyfile(from: [*:0]const u8, to: [*:0]const u8, state: ?std.c.copyfile_state_t, flags: u32) c_int; pub const COPYFILE_STATE_SRC_FD = @as(c_int, 1); pub const COPYFILE_STATE_SRC_FILENAME = @as(c_int, 2); @@ -766,8 +766,15 @@ pub const sockaddr_dl = extern struct { pub usingnamespace @cImport({ @cInclude("sys/spawn.h"); + @cInclude("sys/fcntl.h"); + @cInclude("sys/socket.h"); }); +pub const F = struct { + pub const DUPFD_CLOEXEC = This.F_DUPFD_CLOEXEC; + pub const DUPFD = This.F_DUPFD; +}; + // it turns out preallocating on APFS on an M1 is slower. // so this is a linux-only optimization for now. pub const preallocate_length = std.math.maxInt(u51); diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 2c616353bc73ce..228033dca72493 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -1,4 +1,5 @@ const bun = @import("root").bun; +const Maybe = bun.JSC.Maybe; const WORD = c_ushort; const LARGE_INTEGER = i64; @@ -24,7 +25,7 @@ const sockaddr_un = std.os.linux.sockaddr_un; const BOOL = windows.BOOL; const Env = bun.Environment; -const log = bun.Output.scoped(.uv, false); +pub const log = bun.Output.scoped(.uv, false); pub const CHAR = u8; pub const SHORT = c_short; @@ -290,7 +291,6 @@ pub const uv_once_s = struct_uv_once_s; pub const uv__dirent_s = struct_uv__dirent_s; pub const uv_dirent_s = struct_uv_dirent_s; pub const uv_dir_s = struct_uv_dir_s; -pub const uv_read_s = struct_uv_read_s; pub const uv_shutdown_s = struct_uv_shutdown_s; pub const uv_stream_s = struct_uv_stream_s; pub const uv_tcp_accept_s = struct_uv_tcp_accept_s; @@ -299,11 +299,10 @@ pub const uv_udp_s = struct_uv_udp_s; pub const uv_pipe_accept_s = struct_uv_pipe_accept_s; pub const uv_timer_s = struct_uv_timer_s; pub const uv_write_s = struct_uv_write_s; -pub const uv_pipe_s = struct_uv_pipe_s; pub const uv_tty_s = struct_uv_tty_s; pub const uv_poll_s = struct_uv_poll_s; pub const uv_process_exit_s = struct_uv_process_exit_s; -pub const uv_process_s = struct_uv_process_s; +pub const uv_process_s = Process; pub const uv_fs_event_req_s = struct_uv_fs_event_req_s; pub const uv_fs_event_s = struct_uv_fs_event_s; pub const uv_fs_poll_s = struct_uv_fs_poll_s; @@ -382,6 +381,8 @@ pub const Handle = extern struct { endgame_next: ?*uv_handle_t = null, flags: c_uint, + pub usingnamespace HandleMixin(Handle); + pub const Type = enum(c_uint) { unknown = 0, @"async" = 1, @@ -415,7 +416,9 @@ fn HandleMixin(comptime Type: type) type { pub fn setData(handle: *Type, ptr: ?*anyopaque) void { uv_handle_set_data(@ptrCast(handle), ptr); } - pub fn close(this: *Type, cb: uv_close_cb) void { + pub fn close(this: *Type, cb: *const fn (*Type) callconv(.C) void) void { + if (comptime Env.isDebug) + log("{s}.close({})", .{ bun.meta.typeName(Type), fd(this) }); uv_close(@ptrCast(this), @ptrCast(cb)); } @@ -424,10 +427,14 @@ fn HandleMixin(comptime Type: type) type { } pub fn ref(this: *Type) void { + if (comptime Env.isDebug) + log("{s}.ref({})", .{ bun.meta.typeName(Type), if (comptime Type != Process) fd(this) else Process.getPid(this) }); uv_ref(@ptrCast(this)); } pub fn unref(this: *Type) void { + if (comptime Env.isDebug) + log("{s}.unref({})", .{ bun.meta.typeName(Type), if (comptime Type != Process) fd(this) else Process.getPid(this) }); uv_unref(@ptrCast(this)); } @@ -435,9 +442,22 @@ fn HandleMixin(comptime Type: type) type { return uv_is_closing(@ptrCast(this)) != 0; } + pub fn isClosed(this: *const Type) bool { + return uv_is_closed(@ptrCast(this)); + } + pub fn isActive(this: *const Type) bool { return uv_is_active(@ptrCast(this)) != 0; } + + pub fn fd(this: *const Type) bun.FileDescriptor { + var fd_: uv_os_fd_t = windows.INVALID_HANDLE_VALUE; + _ = uv_fileno(@ptrCast(this), &fd_); + if (fd_ == windows.INVALID_HANDLE_VALUE) + return bun.invalid_fd; + + return bun.FDImpl.fromSystem(fd_).encode(); + } }; } @@ -454,7 +474,7 @@ fn ReqMixin(comptime Type: type) type { uv_req_set_data(@ptrCast(handle), ptr); } pub fn cancel(this: *Type) void { - uv_cancel(@ptrCast(this)); + _ = uv_cancel(@ptrCast(this)); } }; } @@ -584,6 +604,9 @@ pub const Loop = extern struct { this.active_handles += value; } + pub const ref = inc; + pub const unref = dec; + pub fn inc(this: *Loop) void { this.active_handles += 1; } @@ -886,7 +909,7 @@ const union_unnamed_380 = extern union { pub const uv_alloc_cb = ?*const fn (*uv_handle_t, usize, *uv_buf_t) callconv(.C) void; pub const uv_stream_t = struct_uv_stream_s; /// *uv.uv_handle_t is actually *uv_stream_t, just changed to avoid dependency loop error on Zig -pub const uv_read_cb = ?*const fn (*uv_handle_t, isize, *const uv_buf_t) callconv(.C) void; +pub const uv_read_cb = ?*const fn (*uv_handle_t, ReturnCodeI64, *const uv_buf_t) callconv(.C) void; const struct_unnamed_382 = extern struct { overlapped: OVERLAPPED, queued_bytes: usize, @@ -901,7 +924,7 @@ const union_unnamed_381 = extern union { io: struct_unnamed_382, connect: struct_unnamed_383, }; -pub const struct_uv_read_s = extern struct { +pub const Read = extern struct { data: ?*anyopaque, type: uv_req_type, reserved: [6]?*anyopaque, @@ -910,7 +933,7 @@ pub const struct_uv_read_s = extern struct { event_handle: HANDLE, wait_handle: HANDLE, }; -pub const uv_read_t = struct_uv_read_s; +pub const uv_read_t = Read; const struct_unnamed_387 = extern struct { overlapped: OVERLAPPED, queued_bytes: usize, @@ -940,7 +963,7 @@ const struct_unnamed_385 = extern struct { write_reqs_pending: c_uint, shutdown_req: [*c]uv_shutdown_t, }; -pub const uv_connection_cb = ?*const fn ([*c]uv_stream_t, c_int) callconv(.C) void; +pub const uv_connection_cb = ?*const fn (*uv_stream_t, ReturnCode) callconv(.C) void; const struct_unnamed_389 = extern struct { connection_cb: uv_connection_cb, }; @@ -964,6 +987,8 @@ pub const struct_uv_stream_s = extern struct { activecnt: c_int, read_req: uv_read_t, stream: union_unnamed_384, + + pub usingnamespace StreamMixin(@This()); }; const union_unnamed_390 = extern union { fd: c_int, @@ -1165,6 +1190,33 @@ pub const struct_uv_write_s = extern struct { write_buffer: uv_buf_t, event_handle: HANDLE, wait_handle: HANDLE, + + pub fn write(req: *@This(), stream: *uv_stream_t, input: *uv_buf_t, context: anytype, comptime onWrite: ?*const (fn (@TypeOf(context), status: ReturnCode) void)) Maybe(void) { + if (comptime onWrite) |callback| { + const Wrapper = struct { + pub fn uvWriteCb(handler: *uv_write_t, status: ReturnCode) callconv(.C) void { + callback(@ptrCast(@alignCast(handler.data)), status); + } + }; + + req.data = context; + + const rc = uv_write(req, stream, @ptrCast(input), 1, &Wrapper.uvWriteCb); + bun.sys.syslog("uv_write({d}) = {d}", .{ input.len, rc.int() }); + + if (rc.toError(.write)) |err| { + return .{ .err = err }; + } + + return .{ .result = {} }; + } + + const rc = uv_write(req, stream, @ptrCast(input), 1, null); + if (rc.toError(.write)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } }; pub const uv_write_t = struct_uv_write_s; const union_unnamed_415 = extern union { @@ -1186,7 +1238,7 @@ const union_unnamed_405 = extern union { serv: struct_unnamed_406, conn: struct_unnamed_410, }; -pub const struct_uv_pipe_s = extern struct { +pub const Pipe = extern struct { data: ?*anyopaque, loop: ?*uv_loop_t, type: uv_handle_type, @@ -1206,8 +1258,53 @@ pub const struct_uv_pipe_s = extern struct { handle: HANDLE, name: [*]WCHAR, pipe: union_unnamed_405, + + pub usingnamespace StreamMixin(@This()); + + pub fn init(this: *Pipe, loop: *Loop, ipc: bool) Maybe(void) { + if (uv_pipe_init(loop, this, if (ipc) 1 else 0).toError(.pipe)) |err| return .{ .err = err }; + + return .{ .result = {} }; + } + + pub fn open(this: *Pipe, file: uv_file) Maybe(void) { + if (uv_pipe_open(this, file).toError(.open)) |err| return .{ .err = err }; + + return .{ .result = {} }; + } + + pub fn listenNamedPipe(this: *@This(), named_pipe: []const u8, backlog: i32, context: anytype, comptime onClientConnect: *const (fn (@TypeOf(context), ReturnCode) void)) Maybe(void) { + if (this.bind(named_pipe, 0).asErr()) |err| { + return .{ .err = err }; + } + return this.listen(backlog, context, onClientConnect); + } + + pub fn bind(this: *@This(), named_pipe: []const u8, flags: i32) Maybe(void) { + if (uv_pipe_bind2(this, named_pipe.ptr, named_pipe.len, @intCast(flags)).toError(.bind2)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + pub fn connect(this: *@This(), req: *uv_connect_t, name: []const u8, context: anytype, comptime onConnect: *const (fn (@TypeOf(context), ReturnCode) void)) Maybe(void) { + this.data = @ptrCast(context); + const Wrapper = struct { + pub fn uvConnectCb(handle: *uv_connect_t, status: ReturnCode) callconv(.C) void { + onConnect(@ptrCast(@alignCast(handle.data)), status); + } + }; + + if (uv_pipe_connect2(req, this, @ptrCast(name.ptr), name.len, 0, &Wrapper.uvConnectCb).toError(.connect2)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + pub fn setPendingInstancesCount(this: *@This(), count: i32) void { + uv_pipe_pending_instances(this, count); + } }; -pub const uv_pipe_t = struct_uv_pipe_s; const union_unnamed_416 = extern union { fd: c_int, reserved: [4]?*anyopaque, @@ -1265,6 +1362,16 @@ pub const struct_uv_tty_s = extern struct { stream: union_unnamed_417, handle: HANDLE, tty: union_unnamed_420, + + pub fn init(this: *uv_tty_t, loop: *uv_loop_t, fd: uv_file) Maybe(void) { + // last param is ignored + return if (uv_tty_init(loop, this, fd, 0).toError(.open)) |err| + .{ .err = err } + else + .{ .result = {} }; + } + + pub usingnamespace StreamMixin(@This()); }; pub const uv_tty_t = struct_uv_tty_s; const union_unnamed_423 = extern union { @@ -1302,7 +1409,7 @@ const union_unnamed_424 = extern union { fd: c_int, reserved: [4]?*anyopaque, }; -pub const uv_process_t = struct_uv_process_s; +pub const uv_process_t = Process; pub const uv_exit_cb = ?*const fn (*uv_process_t, i64, c_int) callconv(.C) void; const struct_unnamed_426 = extern struct { overlapped: OVERLAPPED, @@ -1325,23 +1432,37 @@ pub const struct_uv_process_exit_s = extern struct { u: union_unnamed_425, next_req: [*c]struct_uv_req_s, }; -pub const struct_uv_process_s = extern struct { - data: ?*anyopaque, - loop: *uv_loop_t, - type: uv_handle_type, - close_cb: uv_close_cb, - handle_queue: struct_uv__queue, - u: union_unnamed_424, - endgame_next: [*c]uv_handle_t, - flags: c_uint, - exit_cb: ?*const fn ([*c]struct_uv_process_s, i64, c_int) callconv(.C) void, - pid: c_int, - exit_req: struct_uv_process_exit_s, - unused: ?*anyopaque, - exit_signal: c_int, - wait_handle: HANDLE, - process_handle: HANDLE, - exit_cb_pending: u8, +pub const Process = extern struct { + data: ?*anyopaque = null, + loop: ?*uv_loop_t = null, + type: uv_handle_type = std.mem.zeroes(uv_handle_type), + close_cb: uv_close_cb = null, + handle_queue: struct_uv__queue = std.mem.zeroes(struct_uv__queue), + u: union_unnamed_424 = std.mem.zeroes(union_unnamed_424), + endgame_next: ?[*]uv_handle_t = null, + flags: c_uint = 0, + exit_cb: uv_exit_cb = null, + pid: c_int = 0, + exit_req: struct_uv_process_exit_s = std.mem.zeroes(struct_uv_process_exit_s), + unused: ?*anyopaque = null, + exit_signal: c_int = 0, + wait_handle: HANDLE = windows.INVALID_HANDLE_VALUE, + process_handle: HANDLE = windows.INVALID_HANDLE_VALUE, + exit_cb_pending: u8 = 0, + + pub fn spawn(handle: *uv_process_t, loop: *uv_loop_t, options: *const uv_process_options_t) ReturnCode { + return uv_spawn(loop, handle, options); + } + + pub usingnamespace HandleMixin(@This()); + + pub fn kill(this: *@This(), signum: c_int) ReturnCode { + return uv_process_kill(@alignCast(@ptrCast(this)), signum); + } + + pub fn getPid(this: *const @This()) c_int { + return uv_process_get_pid(@alignCast(@ptrCast(this))); + } }; const union_unnamed_428 = extern union { fd: c_int, @@ -1510,7 +1631,7 @@ const union_unnamed_441 = extern union { connect: struct_unnamed_443, }; pub const uv_connect_t = struct_uv_connect_s; -pub const uv_connect_cb = ?*const fn ([*c]uv_connect_t, c_int) callconv(.C) void; +pub const uv_connect_cb = ?*const fn (*uv_connect_t, ReturnCode) callconv(.C) void; pub const struct_uv_connect_s = extern struct { data: ?*anyopaque, type: uv_req_type, @@ -1601,13 +1722,17 @@ pub const fs_t = extern struct { sys_errno_: DWORD, file: union_unnamed_450, fs: union_unnamed_451, + pub usingnamespace ReqMixin(@This()); + const UV_FS_CLEANEDUP = 0x0010; pub inline fn deinit(this: *fs_t) void { - this.assert(); + this.assertInitialized(); uv_fs_req_cleanup(this); + this.assertCleanedUp(); } - pub inline fn assert(this: *fs_t) void { + // This assertion tripping is a sign that .deinit() is going to cause invalid memory access + pub inline fn assertInitialized(this: *const fs_t) void { if (bun.Environment.allow_assert) { if (@intFromPtr(this.loop) == 0xAAAAAAAAAAAA0000) { @panic("uv_fs_t was not initialized"); @@ -1615,8 +1740,21 @@ pub const fs_t = extern struct { } } + // This assertion tripping is a sign that a memory leak may happen + pub inline fn assertCleanedUp(this: *const fs_t) void { + if (bun.Environment.allow_assert) { + if (@intFromPtr(this.loop) == 0xAAAAAAAAAAAA0000) { + return; + } + if ((this.flags & UV_FS_CLEANEDUP) != 0) { + return; + } + @panic("uv_fs_t was not cleaned up. it is expected to call .deinit() on the fs_t here."); + } + } + pub inline fn ptrAs(this: *fs_t, comptime T: type) T { - this.assert(); + this.assertInitialized(); return @ptrCast(this.ptr); } @@ -1802,9 +1940,9 @@ pub extern fn uv_loop_configure(loop: *uv_loop_t, option: uv_loop_option, ...) c pub extern fn uv_loop_fork(loop: *uv_loop_t) c_int; pub extern fn uv_run(*uv_loop_t, mode: RunMode) c_int; pub extern fn uv_stop(*uv_loop_t) void; -pub extern fn uv_ref([*c]uv_handle_t) void; -pub extern fn uv_unref([*c]uv_handle_t) void; -pub extern fn uv_has_ref([*c]const uv_handle_t) c_int; +pub extern fn uv_ref(*uv_handle_t) void; +pub extern fn uv_unref(*uv_handle_t) void; +pub extern fn uv_has_ref(*const uv_handle_t) c_int; pub extern fn uv_update_time(*uv_loop_t) void; pub extern fn uv_now([*c]const uv_loop_t) u64; pub extern fn uv_backend_fd([*c]const uv_loop_t) c_int; @@ -1894,19 +2032,19 @@ pub extern fn uv_recv_buffer_size(handle: *uv_handle_t, value: [*c]c_int) c_int; pub extern fn uv_fileno(handle: *const uv_handle_t, fd: [*c]uv_os_fd_t) c_int; pub extern fn uv_buf_init(base: [*]u8, len: c_uint) uv_buf_t; pub extern fn uv_pipe(fds: *[2]uv_file, read_flags: c_int, write_flags: c_int) ReturnCode; -pub extern fn uv_socketpair(@"type": c_int, protocol: c_int, socket_vector: [*c]uv_os_sock_t, flags0: c_int, flags1: c_int) c_int; +pub extern fn uv_socketpair(@"type": c_int, protocol: c_int, socket_vector: [*]uv_os_sock_t, flags0: c_int, flags1: c_int) ReturnCode; pub extern fn uv_stream_get_write_queue_size(stream: [*c]const uv_stream_t) usize; -pub extern fn uv_listen(stream: [*c]uv_stream_t, backlog: c_int, cb: uv_connection_cb) c_int; -pub extern fn uv_accept(server: [*c]uv_stream_t, client: [*c]uv_stream_t) c_int; -pub extern fn uv_read_start([*c]uv_stream_t, alloc_cb: uv_alloc_cb, read_cb: uv_read_cb) c_int; -pub extern fn uv_read_stop([*c]uv_stream_t) c_int; +pub extern fn uv_listen(stream: [*c]uv_stream_t, backlog: c_int, cb: uv_connection_cb) ReturnCode; +pub extern fn uv_accept(server: [*c]uv_stream_t, client: [*c]uv_stream_t) ReturnCode; +pub extern fn uv_read_start(*uv_stream_t, alloc_cb: uv_alloc_cb, read_cb: uv_read_cb) ReturnCode; +pub extern fn uv_read_stop(*uv_stream_t) ReturnCode; pub extern fn uv_write(req: *uv_write_t, handle: *uv_stream_t, bufs: [*]const uv_buf_t, nbufs: c_uint, cb: uv_write_cb) ReturnCode; pub extern fn uv_write2(req: *uv_write_t, handle: *uv_stream_t, bufs: [*]const uv_buf_t, nbufs: c_uint, send_handle: *uv_stream_t, cb: uv_write_cb) ReturnCode; pub extern fn uv_try_write(handle: *uv_stream_t, bufs: [*]const uv_buf_t, nbufs: c_uint) ReturnCode; pub extern fn uv_try_write2(handle: *uv_stream_t, bufs: [*]const uv_buf_t, nbufs: c_uint, send_handle: *uv_stream_t) c_int; pub extern fn uv_is_readable(handle: *const uv_stream_t) c_int; pub extern fn uv_is_writable(handle: *const uv_stream_t) c_int; -pub extern fn uv_stream_set_blocking(handle: *uv_stream_t, blocking: c_int) c_int; +pub extern fn uv_stream_set_blocking(handle: *uv_stream_t, blocking: c_int) ReturnCode; pub extern fn uv_is_closing(handle: *const uv_handle_t) c_int; pub extern fn uv_tcp_init(*uv_loop_t, handle: *uv_tcp_t) c_int; pub extern fn uv_tcp_init_ex(*uv_loop_t, handle: *uv_tcp_t, flags: c_uint) c_int; @@ -1957,7 +2095,7 @@ pub const uv_tty_mode_t = c_uint; pub const UV_TTY_SUPPORTED: c_int = 0; pub const UV_TTY_UNSUPPORTED: c_int = 1; pub const uv_tty_vtermstate_t = c_uint; -pub extern fn uv_tty_init(*uv_loop_t, [*c]uv_tty_t, fd: uv_file, readable: c_int) c_int; +pub extern fn uv_tty_init(*uv_loop_t, [*c]uv_tty_t, fd: uv_file, readable: c_int) ReturnCode; pub extern fn uv_tty_set_mode([*c]uv_tty_t, mode: uv_tty_mode_t) c_int; pub extern fn uv_tty_reset_mode() c_int; pub extern fn uv_tty_get_winsize([*c]uv_tty_t, width: [*c]c_int, height: [*c]c_int) c_int; @@ -1966,18 +2104,18 @@ pub extern fn uv_tty_get_vterm_state(state: [*c]uv_tty_vtermstate_t) c_int; pub extern fn uv_guess_handle(file: uv_file) uv_handle_type; pub const UV_PIPE_NO_TRUNCATE: c_int = 1; const enum_unnamed_462 = c_uint; -pub extern fn uv_pipe_init(*uv_loop_t, handle: *uv_pipe_t, ipc: c_int) c_int; -pub extern fn uv_pipe_open([*c]uv_pipe_t, file: uv_file) ReturnCode; -pub extern fn uv_pipe_bind(handle: *uv_pipe_t, name: [*]const u8) c_int; -pub extern fn uv_pipe_bind2(handle: *uv_pipe_t, name: [*]const u8, namelen: usize, flags: c_uint) c_int; -pub extern fn uv_pipe_connect(req: [*c]uv_connect_t, handle: *uv_pipe_t, name: [*]const u8, cb: uv_connect_cb) void; -pub extern fn uv_pipe_connect2(req: [*c]uv_connect_t, handle: *uv_pipe_t, name: [*]const u8, namelen: usize, flags: c_uint, cb: uv_connect_cb) c_int; -pub extern fn uv_pipe_getsockname(handle: *const uv_pipe_t, buffer: [*]u8, size: [*c]usize) c_int; -pub extern fn uv_pipe_getpeername(handle: *const uv_pipe_t, buffer: [*]u8, size: [*c]usize) c_int; -pub extern fn uv_pipe_pending_instances(handle: *uv_pipe_t, count: c_int) void; -pub extern fn uv_pipe_pending_count(handle: *uv_pipe_t) c_int; -pub extern fn uv_pipe_pending_type(handle: *uv_pipe_t) uv_handle_type; -pub extern fn uv_pipe_chmod(handle: *uv_pipe_t, flags: c_int) c_int; +pub extern fn uv_pipe_init(*uv_loop_t, handle: *Pipe, ipc: c_int) ReturnCode; +pub extern fn uv_pipe_open(*Pipe, file: uv_file) ReturnCode; +pub extern fn uv_pipe_bind(handle: *Pipe, name: [*]const u8) c_int; +pub extern fn uv_pipe_bind2(handle: *Pipe, name: [*]const u8, namelen: usize, flags: c_uint) ReturnCode; +pub extern fn uv_pipe_connect(req: [*c]uv_connect_t, handle: *Pipe, name: [*]const u8, cb: uv_connect_cb) void; +pub extern fn uv_pipe_connect2(req: [*c]uv_connect_t, handle: *Pipe, name: [*]const u8, namelen: usize, flags: c_uint, cb: uv_connect_cb) ReturnCode; +pub extern fn uv_pipe_getsockname(handle: *const Pipe, buffer: [*]u8, size: [*c]usize) c_int; +pub extern fn uv_pipe_getpeername(handle: *const Pipe, buffer: [*]u8, size: [*c]usize) c_int; +pub extern fn uv_pipe_pending_instances(handle: *Pipe, count: c_int) void; +pub extern fn uv_pipe_pending_count(handle: *Pipe) c_int; +pub extern fn uv_pipe_pending_type(handle: *Pipe) uv_handle_type; +pub extern fn uv_pipe_chmod(handle: *Pipe, flags: c_int) c_int; pub const UV_READABLE: c_int = 1; pub const UV_WRITABLE: c_int = 2; pub const UV_DISCONNECT: c_int = 4; @@ -2008,17 +2146,53 @@ pub extern fn uv_timer_get_due_in(handle: *const uv_timer_t) u64; pub extern fn uv_getaddrinfo(loop: *uv_loop_t, req: *uv_getaddrinfo_t, getaddrinfo_cb: uv_getaddrinfo_cb, node: [*:0]const u8, service: [*:0]const u8, hints: ?*const anyopaque) ReturnCode; pub extern fn uv_freeaddrinfo(ai: *anyopaque) void; pub extern fn uv_getnameinfo(loop: *uv_loop_t, req: [*c]uv_getnameinfo_t, getnameinfo_cb: uv_getnameinfo_cb, addr: [*c]const sockaddr, flags: c_int) c_int; -pub const UV_IGNORE: c_int = 0; -pub const UV_CREATE_PIPE: c_int = 1; -pub const UV_INHERIT_FD: c_int = 2; -pub const UV_INHERIT_STREAM: c_int = 4; -pub const UV_READABLE_PIPE: c_int = 16; -pub const UV_WRITABLE_PIPE: c_int = 32; -pub const UV_NONBLOCK_PIPE: c_int = 64; -pub const UV_OVERLAPPED_PIPE: c_int = 64; +pub const UV_IGNORE = 0; +pub const UV_CREATE_PIPE = 1; +pub const UV_INHERIT_FD = 2; +pub const UV_INHERIT_STREAM = 4; +pub const UV_READABLE_PIPE = 16; +pub const UV_WRITABLE_PIPE = 32; +pub const UV_NONBLOCK_PIPE = 64; +pub const UV_OVERLAPPED_PIPE = 64; pub const uv_stdio_flags = c_uint; +pub const StdioFlags = struct { + pub const ignore = UV_IGNORE; + pub const create_pipe = UV_CREATE_PIPE; + pub const inherit_fd = UV_INHERIT_FD; + pub const inherit_stream = UV_INHERIT_STREAM; + pub const readable_pipe = UV_READABLE_PIPE; + pub const writable_pipe = UV_WRITABLE_PIPE; + pub const nonblock_pipe = UV_NONBLOCK_PIPE; + pub const overlapped_pipe = UV_OVERLAPPED_PIPE; +}; + +pub fn socketpair(stdio_flag_1: uv_stdio_flags, stdio_flag_2: uv_stdio_flags) Maybe([2]*anyopaque) { + var pair: [2]uv_os_sock_t = undefined; + // https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket + const SOCK_STREAM = 1; + + if (uv_socketpair(0, SOCK_STREAM, &pair, stdio_flag_1, stdio_flag_2).toError(.open)) |err| { + return .{ .err = err }; + } + + return .{ .result = pair }; +} +pub usingnamespace struct { + pub fn pipe(stdio_flag_1: uv_stdio_flags, stdio_flag_2: uv_stdio_flags) Maybe([2]*anyopaque) { + var pair: [2]uv_file = undefined; + // https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket + const SOCK_STREAM = 1; + + if (uv_socketpair(0, SOCK_STREAM, &pair, stdio_flag_1, stdio_flag_2).toError(.open)) |err| { + return .{ .err = err }; + } + + return .{ .result = pair }; + } +}; + const union_unnamed_463 = extern union { - stream: [*c]uv_stream_t, + stream: *uv_stream_t, fd: c_int, }; pub const struct_uv_stdio_container_s = extern struct { @@ -2029,7 +2203,9 @@ pub const uv_stdio_container_t = struct_uv_stdio_container_s; pub const uv_process_options_t = extern struct { exit_cb: uv_exit_cb, file: [*:0]const u8, - args: [*:null]?[*:0]u8, + // TODO(@paperdave): upstream changing libuv's args to const + // it is not mutated in any of their code + args: [*:null]?[*:0]const u8, env: [*:null]?[*:0]const u8, cwd: [*:0]const u8, flags: c_uint, @@ -2047,9 +2223,9 @@ pub const UV_PROCESS_WINDOWS_HIDE_CONSOLE: c_int = 32; pub const UV_PROCESS_WINDOWS_HIDE_GUI: c_int = 64; pub const enum_uv_process_flags = c_uint; pub extern fn uv_spawn(loop: *uv_loop_t, handle: *uv_process_t, options: *const uv_process_options_t) ReturnCode; -pub extern fn uv_process_kill([*c]uv_process_t, signum: c_int) ReturnCode; +pub extern fn uv_process_kill(*uv_process_t, signum: c_int) ReturnCode; pub extern fn uv_kill(pid: c_int, signum: c_int) ReturnCode; -pub extern fn uv_process_get_pid([*c]const uv_process_t) uv_pid_t; +pub extern fn uv_process_get_pid(*const uv_process_t) uv_pid_t; pub extern fn uv_queue_work(loop: *uv_loop_t, req: [*c]uv_work_t, work_cb: uv_work_cb, after_work_cb: uv_after_work_cb) c_int; pub extern fn uv_cancel(req: [*c]uv_req_t) c_int; pub const UV_DIRENT_UNKNOWN: c_int = 0; @@ -2299,7 +2475,7 @@ pub const union_uv_any_handle = extern union { fs_poll: uv_fs_poll_t, handle: uv_handle_t, idle: uv_idle_t, - pipe: uv_pipe_t, + pipe: Pipe, poll: uv_poll_t, prepare: uv_prepare_t, process: uv_process_t, @@ -2332,7 +2508,9 @@ pub fn uv_is_closed(handle: *const uv_handle_t) bool { return (handle.flags & UV_HANDLE_CLOSED != 0); } -pub fn translateUVErrorToE(code: anytype) bun.C.E { +pub fn translateUVErrorToE(code_in: anytype) bun.C.E { + const code: c_int = @intCast(code_in); + return switch (code) { UV_EPERM => bun.C.E.PERM, UV_ENOENT => bun.C.E.NOENT, @@ -2407,6 +2585,9 @@ pub fn translateUVErrorToE(code: anytype) bun.C.E { } pub const ReturnCode = enum(c_int) { + zero = 0, + _, + pub fn format(this: ReturnCode, comptime fmt_: []const u8, options_: std.fmt.FormatOptions, writer: anytype) !void { _ = fmt_; _ = options_; @@ -2414,7 +2595,7 @@ pub const ReturnCode = enum(c_int) { if (this.errEnum()) |err| { try writer.writeAll(@tagName(err)); } else { - try writer.print("{d}", .{this}); + try writer.print("{d}", .{@intFromEnum(this)}); } } @@ -2422,6 +2603,17 @@ pub const ReturnCode = enum(c_int) { return @intFromEnum(this); } + pub fn toError(this: ReturnCode, syscall: bun.sys.Tag) ?bun.sys.Error { + if (this.errno()) |e| { + return .{ + .errno = e, + .syscall = syscall, + }; + } + + return null; + } + pub inline fn errno(this: ReturnCode) ?@TypeOf(@intFromEnum(bun.C.E.ACCES)) { return if (this.int() < 0) switch (this.int()) { @@ -2507,8 +2699,13 @@ pub const ReturnCode = enum(c_int) { } }; -pub const ReturnCodeI64 = extern struct { - value: i64, +pub const ReturnCodeI64 = enum(i64) { + zero = 0, + _, + + pub fn init(i: i64) ReturnCodeI64 { + return @enumFromInt(i); + } pub fn format(this: ReturnCodeI64, comptime fmt_: []const u8, options_: std.fmt.FormatOptions, writer: anytype) !void { _ = fmt_; @@ -2517,61 +2714,193 @@ pub const ReturnCodeI64 = extern struct { if (this.errEnum()) |err| { try writer.writeAll(@tagName(err)); } else { - try writer.print("{d}", .{this.value}); + try writer.print("{d}", .{@intFromEnum(this)}); + } + } + + pub fn toError(this: ReturnCodeI64, syscall: bun.sys.Tag) ?bun.sys.Error { + if (this.errno()) |e| { + return .{ + .errno = e, + .syscall = syscall, + }; } + + return null; } pub inline fn errno(this: ReturnCodeI64) ?@TypeOf(@intFromEnum(bun.C.E.ACCES)) { - return if (this.value < 0) - @as(u16, @intCast(-this.value)) + return if (@intFromEnum(this) < 0) + @as(u16, @intCast(-@intFromEnum(this))) else null; } pub inline fn errEnum(this: ReturnCodeI64) ?bun.C.E { - return if (this.value < 0) - (translateUVErrorToE(this.value)) + return if (@intFromEnum(this) < 0) + (translateUVErrorToE(@intFromEnum(this))) else null; } - comptime { - std.debug.assert(@as(i64, @bitCast(ReturnCodeI64{ .value = 4021000000000 })) == 4021000000000); + pub inline fn int(this: ReturnCodeI64) i64 { + return @intFromEnum(this); + } + + pub fn toFD(this: ReturnCodeI64) bun.FileDescriptor { + return bun.toFD(@as(i32, @truncate(this.int()))); } }; pub const addrinfo = std.os.windows.ws2_32.addrinfo; -fn WriterMixin(comptime Type: type) type { +// https://docs.libuv.org/en/v1.x/stream.html +fn StreamMixin(comptime Type: type) type { return struct { - pub fn write(mixin: *Type, input: []const u8, context: anytype, comptime onWrite: ?*const (fn (*@TypeOf(context), status: ReturnCode) void)) ReturnCode { + pub usingnamespace HandleMixin(Type); + + pub fn getWriteQueueSize(this: *Type) usize { + return uv_stream_get_write_queue_size(@ptrCast(this)); + } + + pub fn listen(this: *Type, backlog: i32, context: anytype, comptime onConnect: *const (fn (@TypeOf(context), ReturnCode) void)) Maybe(void) { + this.data = @ptrCast(context); + const Wrapper = struct { + pub fn uvConnectCb(handle: *uv_stream_t, status: ReturnCode) callconv(.C) void { + onConnect(@ptrCast(@alignCast(handle.data)), status); + } + }; + if (uv_listen(@ptrCast(this), backlog, &Wrapper.uvConnectCb).toError(.listen)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + pub fn accept(this: *Type, client: *Type) Maybe(void) { + if (uv_accept(@ptrCast(this), @ptrCast(client)).toError(.accept)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + pub fn readStart( + this: *Type, + context: anytype, + comptime alloc_cb: *const (fn (@TypeOf(context), suggested_size: usize) []u8), + comptime error_cb: *const (fn (@TypeOf(context), err: bun.C.E) void), + comptime read_cb: *const (fn (@TypeOf(context), data: []const u8) void), + ) Maybe(void) { + const Context = @TypeOf(context); + this.data = @ptrCast(context); + const Wrapper = struct { + pub fn uvAllocb(req: *uv_stream_t, suggested_size: usize, buffer: *uv_buf_t) callconv(.C) void { + const context_data: Context = @ptrCast(@alignCast(req.data)); + buffer.* = uv_buf_t.init(alloc_cb(context_data, suggested_size)); + } + pub fn uvReadcb(req: *uv_stream_t, nreads: isize, buffer: *uv_buf_t) callconv(.C) void { + const context_data: Context = @ptrCast(@alignCast(req.data)); + if (nreads == 0) return; // EAGAIN or EWOULDBLOCK + if (nreads < 0) { + req.readStop(); + error_cb(context_data, ReturnCodeI64.init(nreads).errEnum() orelse bun.C.E.CANCELED); + } else { + read_cb(context_data, buffer.slice()); + } + } + }; + + if (uv_read_start(@ptrCast(this), @ptrCast(&Wrapper.uvAllocb), @ptrCast(&Wrapper.uvReadcb)).toError(.listen)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + pub fn readStop(this: *Type) void { + // always succeed see https://docs.libuv.org/en/v1.x/stream.html#c.uv_read_stop + _ = uv_read_stop(@ptrCast(this)); + } + + pub fn write(this: *Type, input: *uv_buf_t, context: anytype, comptime onWrite: ?*const (fn (@TypeOf(context), status: ReturnCode) void)) Maybe(void) { if (comptime onWrite) |callback| { const Context = @TypeOf(context); - var data = bun.new(uv_write_t); - data.data = context; const Wrapper = struct { - uv_data: uv_write_t, - context: Context, - buf: uv_buf_t, - pub fn uvWriteCb(req: *uv_write_t, status: ReturnCode) callconv(.C) void { - const this: *@This() = @fieldParentPtr(@This(), "uv_data", req); - const context_data = this.context; - bun.destroy(this); - callback(context_data, @enumFromInt(status)); + const context_data: Context = @ptrCast(@alignCast(req.data)); + bun.sys.syslog("uv_write({d}) = {d}", .{ req.write_buffer.len, status.int() }); + bun.destroy(req); + callback(context_data, status); } }; - var wrap = bun.new(Wrapper, Wrapper{ - .wrapper = undefined, - .context = context, - .buf = uv_buf_t.init(input), - }); + var uv_data = bun.new(uv_write_t, std.mem.zeroes(uv_write_t)); + uv_data.data = context; + + if (uv_write(uv_data, @ptrCast(this), @ptrCast(input), 1, &Wrapper.uvWriteCb).toError(.write)) |err| { + return .{ .err = err }; + } + return .{ .result = {} }; + } + + var req: uv_write_t = std.mem.zeroes(uv_write_t); + if (uv_write(&req, this, @ptrCast(input), 1, null).toError(.write)) |err| { + return .{ .err = err }; + } + + return .{ .result = {} }; + } + + pub fn tryWrite(this: *Type, input: []const u8) Maybe(usize) { + const rc = uv_try_write(@ptrCast(this), @ptrCast(&uv_buf_t.init(input)), 1); + if (rc.toError(.try_write)) |err| { + return .{ .err = err }; + } + return .{ .result = @intCast(rc.int()) }; + } + + pub fn tryWrite2(this: *Type, input: []const u8, send_handle: *uv_stream_t) ReturnCode { + const rc = uv_try_write2(@ptrCast(this), @ptrCast(&uv_buf_t.init(input)), 1, send_handle); + if (rc.toError(.try_write2)) |err| { + return .{ .err = err }; + } + return .{ .result = @intCast(rc.int()) }; + } + + pub fn isReadable(this: *Type) bool { + return uv_is_readable(@ptrCast(this)) != 0; + } + + pub fn isWritable(this: *@This()) bool { + return uv_is_writable(@ptrCast(this)) != 0; + } + }; +} + +pub fn StreamWriterMixin(comptime Type: type, comptime pipe_field_name: std.meta.FieldEnum(Type), comptime uv_write_t_field_name: std.meta.FieldEnum(Type)) type { + return struct { + fn __get_pipe(this: *@This()) *uv_stream_t { + comptime { + switch (@TypeOf(@field(this, @tagName(@tagName(pipe_field_name))))) { + Pipe, uv_tcp_t, uv_tty_t => {}, + else => @compileError("StreamWriterMixin only works with Pipe, uv_tcp_t, uv_tty_t"), + } + } + + return @ptrCast(&@field(this, @tagName(@tagName(pipe_field_name)))); + } + + fn uv_on_write_cb(req: *uv_write_t, status: ReturnCode) callconv(.C) void { + var this: *Type = @fieldParentPtr(Type, @tagName(uv_write_t_field_name), req); + this.onWrite(if (status.toError(.send)) |err| .{ .err = err } else .{ .result = @intCast(status.int()) }); + } - return uv_write(&wrap.uv_data, @ptrCast(mixin), @ptrCast(&wrap.buf), 1, &Wrapper.uvWriteCb); + pub fn write(this: *@This(), input: []const u8) void { + if (comptime Env.allow_assert) { + if (!this.isStreamWritable()) { + @panic("StreamWriterMixin.write: stream is not writable. This is a bug in Bun."); + } } - return uv_write(null, mixin, @ptrCast(&uv_buf_t.init(input)), 1, null); + __get_pipe(this).write(input, this, &uv_on_write_cb); } }; } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 20c0b20f870582..80ca05b3f15c08 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2502,7 +2502,8 @@ extern fn uws_app_listen_domain_with_options( ?*anyopaque, ) void; -pub const UVLoop = extern struct { +/// This extends off of uws::Loop on Windows +pub const WindowsLoop = extern struct { const uv = bun.windows.libuv; internal_loop_data: InternalLoopData align(16), @@ -2512,46 +2513,43 @@ pub const UVLoop = extern struct { pre: *uv.uv_prepare_t, check: *uv.uv_check_t, - pub fn init() *UVLoop { + pub fn get() *WindowsLoop { return uws_get_loop_with_native(bun.windows.libuv.Loop.get()); } - extern fn uws_get_loop_with_native(*anyopaque) *UVLoop; + extern fn uws_get_loop_with_native(*anyopaque) *WindowsLoop; - pub fn iterationNumber(this: *const UVLoop) c_longlong { + pub fn iterationNumber(this: *const WindowsLoop) c_longlong { return this.internal_loop_data.iteration_nr; } - pub fn addActive(this: *const UVLoop, val: u32) void { + pub fn addActive(this: *const WindowsLoop, val: u32) void { this.uv_loop.addActive(val); } - pub fn subActive(this: *const UVLoop, val: u32) void { + pub fn subActive(this: *const WindowsLoop, val: u32) void { this.uv_loop.subActive(val); } - pub fn isActive(this: *const UVLoop) bool { + pub fn isActive(this: *const WindowsLoop) bool { return this.uv_loop.isActive(); } - pub fn get() *UVLoop { - return @ptrCast(uws_get_loop()); - } - pub fn wakeup(this: *UVLoop) void { + pub fn wakeup(this: *WindowsLoop) void { us_wakeup_loop(this); } pub const wake = wakeup; - pub fn tickWithTimeout(this: *UVLoop, _: i64) void { + pub fn tickWithTimeout(this: *WindowsLoop, _: i64) void { us_loop_run(this); } - pub fn tickWithoutIdle(this: *UVLoop) void { + pub fn tickWithoutIdle(this: *WindowsLoop) void { us_loop_pump(this); } - pub fn create(comptime Handler: anytype) *UVLoop { + pub fn create(comptime Handler: anytype) *WindowsLoop { return us_create_loop( null, Handler.wakeup, @@ -2561,7 +2559,7 @@ pub const UVLoop = extern struct { ).?; } - pub fn run(this: *UVLoop) void { + pub fn run(this: *WindowsLoop) void { us_loop_run(this); } @@ -2569,14 +2567,17 @@ pub const UVLoop = extern struct { pub const tick = run; pub const wait = run; - pub fn inc(this: *UVLoop) void { + pub fn inc(this: *WindowsLoop) void { this.uv_loop.inc(); } - pub fn dec(this: *UVLoop) void { + pub fn dec(this: *WindowsLoop) void { this.uv_loop.dec(); } + pub const ref = inc; + pub const unref = dec; + pub fn nextTick(this: *Loop, comptime UserType: type, user_data: UserType, comptime deferCallback: fn (ctx: UserType) void) void { const Handler = struct { pub fn callback(data: *anyopaque) callconv(.C) void { @@ -2602,7 +2603,7 @@ pub const UVLoop = extern struct { } }; -pub const Loop = if (bun.Environment.isWindows) UVLoop else PosixLoop; +pub const Loop = if (bun.Environment.isWindows) WindowsLoop else PosixLoop; extern fn uws_get_loop() *Loop; extern fn us_create_loop( @@ -2629,7 +2630,7 @@ extern fn us_socket_pair( fds: *[2]LIBUS_SOCKET_DESCRIPTOR, ) ?*Socket; -extern fn us_socket_from_fd( +pub extern fn us_socket_from_fd( ctx: *SocketContext, ext_size: c_int, fd: LIBUS_SOCKET_DESCRIPTOR, diff --git a/src/env_loader.zig b/src/env_loader.zig index 405595c9c6c937..1e776a38f9d20b 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -45,6 +45,10 @@ pub const Loader = struct { did_load_process: bool = false, reject_unauthorized: ?bool = null, + pub fn iterator(this: *const Loader) Map.HashTable.Iterator { + return this.map.iterator(); + } + pub fn has(this: *const Loader, input: []const u8) bool { const value = this.get(input) orelse return false; if (value.len == 0) return false; @@ -1152,12 +1156,12 @@ pub const Map = struct { return result[0..].ptr; } - pub inline fn init(allocator: std.mem.Allocator) Map { - return Map{ .map = HashTable.init(allocator) }; + pub fn iterator(this: *const Map) HashTable.Iterator { + return this.map.iterator(); } - pub inline fn iterator(this: *Map) HashTable.Iterator { - return this.map.iterator(); + pub inline fn init(allocator: std.mem.Allocator) Map { + return Map{ .map = HashTable.init(allocator) }; } pub inline fn put(this: *Map, key: string, value: string) !void { diff --git a/src/fd.zig b/src/fd.zig index febaa8799856e1..f0d516e9b5cd7e 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -10,7 +10,7 @@ const libuv = bun.windows.libuv; const allow_assert = env.allow_assert; -const log = bun.Output.scoped(.fs, false); +const log = bun.sys.syslog; fn handleToNumber(handle: FDImpl.System) FDImpl.SystemAsInt { if (env.os == .windows) { // intCast fails if 'fd > 2^62' @@ -34,13 +34,15 @@ fn numberToHandle(handle: FDImpl.SystemAsInt) FDImpl.System { pub fn uv_get_osfhandle(in: c_int) libuv.uv_os_fd_t { const out = libuv.uv_get_osfhandle(in); - log("uv_get_osfhandle({d}) = {d}", .{ in, @intFromPtr(out) }); + // TODO: this is causing a dead lock because is also used on fd format + // log("uv_get_osfhandle({d}) = {d}", .{ in, @intFromPtr(out) }); return out; } pub fn uv_open_osfhandle(in: libuv.uv_os_fd_t) c_int { const out = libuv.uv_open_osfhandle(in); - log("uv_open_osfhandle({d}) = {d}", .{ @intFromPtr(in), out }); + // TODO: this is causing a dead lock because is also used on fd format + // log("uv_open_osfhandle({d}) = {d}", .{ @intFromPtr(in), out }); return out; } @@ -217,7 +219,7 @@ pub const FDImpl = packed struct { // Format the file descriptor for logging BEFORE closing it. // Otherwise the file descriptor is always invalid after closing it. - var buf: [1050]u8 = undefined; + var buf: if (env.isDebug) [1050]u8 else void = undefined; const this_fmt = if (env.isDebug) std.fmt.bufPrint(&buf, "{}", .{this}) catch unreachable; const result: ?bun.sys.Error = switch (env.os) { @@ -307,6 +309,11 @@ pub const FDImpl = packed struct { } pub fn format(this: FDImpl, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + if (!this.isValid()) { + try writer.writeAll("[invalid_fd]"); + return; + } + if (fmt.len != 0) { // The reason for this error is because formatting FD as an integer on windows is // ambiguous and almost certainly a mistake. You probably meant to format fd.cast(). @@ -321,10 +328,6 @@ pub const FDImpl = packed struct { @compileError("invalid format string for FDImpl.format. must be empty like '{}'"); } - if (!this.isValid()) { - try writer.writeAll("[invalid_fd]"); - return; - } switch (env.os) { else => { const fd = this.system(); @@ -351,7 +354,7 @@ pub const FDImpl = packed struct { return try writer.print("{d}[cwd handle]", .{this.value.as_system}); } else print_with_path: { var fd_path: bun.WPathBuffer = undefined; - const path = std.os.windows.GetFinalPathNameByHandle(handle, .{ .volume_name = .Dos }, &fd_path) catch break :print_with_path; + const path = std.os.windows.GetFinalPathNameByHandle(handle, .{ .volume_name = .Nt }, &fd_path) catch break :print_with_path; return try writer.print("{d}[{}]", .{ this.value.as_system, bun.fmt.utf16(path), diff --git a/src/install/install.zig b/src/install/install.zig index 4cb370af304d14..5c8bb484deb7ac 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1883,52 +1883,6 @@ pub const CacheLevel = struct { use_etag: bool, use_last_modified: bool, }; -const Waker = if (Environment.isPosix) bun.Async.Waker else *bun.uws.UVLoop; - -const Waiter = struct { - onWait: *const fn (this: *anyopaque) anyerror!usize, - onWake: *const fn (this: *anyopaque) void, - ctx: *anyopaque, - - pub fn init( - ctx: anytype, - comptime onWait: *const fn (this: @TypeOf(ctx)) anyerror!usize, - comptime onWake: *const fn (this: @TypeOf(ctx)) void, - ) Waiter { - return Waiter{ - .ctx = @ptrCast(ctx), - .onWait = @alignCast(@ptrCast(@as(*const anyopaque, @ptrCast(onWait)))), - .onWake = @alignCast(@ptrCast(@as(*const anyopaque, @ptrCast(onWake)))), - }; - } - - pub fn wait(this: *Waiter) !usize { - return this.onWait(this.ctx); - } - - pub fn wake(this: *Waiter) void { - this.onWake(this.ctx); - } - - pub fn fromUWSLoop(loop: *uws.Loop) Waiter { - const Handlers = struct { - fn onWait(uws_loop: *uws.Loop) !usize { - uws_loop.run(); - return 0; - } - - fn onWake(uws_loop: *uws.Loop) void { - uws_loop.wakeup(); - } - }; - - return Waiter.init( - loop, - Handlers.onWait, - Handlers.onWake, - ); - } -}; // We can't know all the packages we need until we've downloaded all the packages // The easy way would be: @@ -2015,14 +1969,11 @@ pub const PackageManager = struct { peer_dependencies: std.fifo.LinearFifo(DependencyID, .Dynamic) = std.fifo.LinearFifo(DependencyID, .Dynamic).init(default_allocator), - /// Do not use directly outside of wait or wake - uws_event_loop: *uws.Loop, - - file_poll_store: bun.Async.FilePoll.Store, - // name hash from alias package name -> aliased package dependency version info known_npm_aliases: NpmAliasMap = .{}, + event_loop: JSC.AnyEventLoop, + // During `installPackages` we learn exactly what dependencies from --trust // actually have scripts to run, and we add them to this list trusted_deps_to_add_to_package_json: std.ArrayListUnmanaged(string) = .{}, @@ -2098,7 +2049,7 @@ pub const PackageManager = struct { }; pub fn hasEnoughTimePassedBetweenWaitingMessages() bool { - const iter = instance.uws_event_loop.iterationNumber(); + const iter = instance.event_loop.loop().iterationNumber(); if (TimePasser.last_time < iter) { TimePasser.last_time = iter; return true; @@ -2152,7 +2103,7 @@ pub const PackageManager = struct { var bun_path: string = ""; RunCommand.createFakeTemporaryNodeExecutable(&PATH, &bun_path) catch break :brk; try this.env.map.put("PATH", PATH.items); - _ = try this.env.loadNodeJSConfig(this_bundler.fs, bun.default_allocator.dupe(u8, RunCommand.bun_node_dir) catch bun.outOfMemory()); + _ = try this.env.loadNodeJSConfig(this_bundler.fs, bun.default_allocator.dupe(u8, bun_path) catch bun.outOfMemory()); } } @@ -2210,22 +2161,20 @@ pub const PackageManager = struct { } _ = this.wait_count.fetchAdd(1, .Monotonic); - this.uws_event_loop.wakeup(); + this.event_loop.wakeup(); + } + + fn hasNoMorePendingLifecycleScripts(this: *PackageManager) bool { + return this.pending_lifecycle_script_tasks.load(.Monotonic) == 0; } pub fn tickLifecycleScripts(this: *PackageManager) void { - if (this.pending_lifecycle_script_tasks.load(.Monotonic) > 0) { - this.uws_event_loop.tickWithoutIdle(); - } + this.event_loop.tickOnce(this); } pub fn sleep(this: *PackageManager) void { - if (this.wait_count.swap(0, .Monotonic) > 0) { - this.tickLifecycleScripts(); - return; - } Output.flush(); - this.uws_event_loop.tick(); + this.event_loop.tick(this, hasNoMorePendingLifecycleScripts); } const DependencyToEnqueue = union(enum) { @@ -2603,10 +2552,9 @@ pub const PackageManager = struct { } pub fn ensureTempNodeGypScript(this: *PackageManager) !void { - if (comptime Environment.isWindows) { - @panic("TODO: command prompt version of temp node-gyp script"); + if (Environment.isWindows) { + Output.debug("TODO: VERIFY ensureTempNodeGypScript WORKS!!", .{}); } - if (this.node_gyp_tempdir_name.len > 0) return; const tempdir = this.getTemporaryDirectory(); @@ -2627,13 +2575,25 @@ pub const PackageManager = struct { }; defer node_gyp_tempdir.close(); - var node_gyp_file = node_gyp_tempdir.createFile("node-gyp", .{ .mode = 0o777 }) catch |err| { + const file_name = switch (Environment.os) { + else => "node-gyp", + .windows => "node-gyp.cmd", + }; + const mode = switch (Environment.os) { + else => 0o755, + .windows => 0, // windows does not have an executable bit + }; + + var node_gyp_file = node_gyp_tempdir.createFile(file_name, .{ .mode = mode }) catch |err| { Output.prettyErrorln("error: {s} creating node-gyp tempdir", .{@errorName(err)}); Global.crash(); }; defer node_gyp_file.close(); - var bytes: string = "#!/usr/bin/env node\nrequire(\"child_process\").spawnSync(\"bun\",[\"x\",\"node-gyp\",...process.argv.slice(2)],{stdio:\"inherit\"})"; + var bytes: string = switch (Environment.os) { + else => "#!/usr/bin/env node\nrequire(\"child_process\").spawnSync(\"bun\",[\"x\",\"node-gyp\",...process.argv.slice(2)],{stdio:\"inherit\"})", + .windows => "@node -e \"require('child_process').spawnSync('bun',['x','node-gyp',...process.argv.slice(2)],{stdio:'inherit'})\"", + }; var index: usize = 0; while (index < bytes.len) { switch (bun.sys.write(bun.toFD(node_gyp_file.handle), bytes[index..])) { @@ -2641,7 +2601,7 @@ pub const PackageManager = struct { index += written; }, .err => |err| { - Output.prettyErrorln("error: {s} writing to node-gyp file", .{@tagName(err.getErrno())}); + Output.prettyErrorln("error: {s} writing to " ++ file_name ++ " file", .{@tagName(err.getErrno())}); Global.crash(); }, } @@ -6680,7 +6640,7 @@ pub const PackageManager = struct { } if (env.get("BUN_FEATURE_FLAG_FORCE_WAITER_THREAD") != null) { - JSC.Subprocess.WaiterThread.setShouldUseWaiterThread(); + bun.spawn.WaiterThread.setShouldUseWaiterThread(); } if (PackageManager.verbose_install) { @@ -6721,11 +6681,12 @@ pub const PackageManager = struct { .root_package_json_file = package_json_file, .workspaces = workspaces, // .progress - .uws_event_loop = uws.Loop.get(), - .file_poll_store = bun.Async.FilePoll.Store.init(ctx.allocator), + .event_loop = .{ + .mini = JSC.MiniEventLoop.init(bun.default_allocator), + }, }; manager.lockfile = try ctx.allocator.create(Lockfile); - + JSC.MiniEventLoop.global = &manager.event_loop.mini; if (!manager.options.enable.cache) { manager.options.enable.manifest_cache = false; manager.options.enable.manifest_cache_control = false; @@ -6811,8 +6772,9 @@ pub const PackageManager = struct { .resolve_tasks = TaskChannel.init(), .lockfile = undefined, .root_package_json_file = undefined, - .uws_event_loop = uws.Loop.get(), - .file_poll_store = bun.Async.FilePoll.Store.init(allocator), + .event_loop = .{ + .js = JSC.VirtualMachine.get().eventLoop(), + }, .workspaces = std.StringArrayHashMap(Semver.Version).init(allocator), }; manager.lockfile = try allocator.create(Lockfile); @@ -8200,9 +8162,6 @@ pub const PackageManager = struct { /// Increments the number of installed packages for a tree id and runs available scripts /// if the tree is finished. pub fn incrementTreeInstallCount(this: *PackageInstaller, tree_id: Lockfile.Tree.Id, comptime log_level: Options.LogLevel) void { - if (comptime Environment.isWindows) { - return bun.todo(@src(), {}); - } if (comptime Environment.allow_assert) { std.debug.assert(tree_id != Lockfile.Tree.invalid_id); } @@ -8229,9 +8188,6 @@ pub const PackageManager = struct { } pub fn runAvailableScripts(this: *PackageInstaller, comptime log_level: Options.LogLevel) void { - if (comptime Environment.isWindows) { - return bun.todo(@src(), {}); - } var i: usize = this.pending_lifecycle_scripts.items.len; while (i > 0) { i -= 1; @@ -8268,9 +8224,6 @@ pub const PackageManager = struct { } pub fn completeRemainingScripts(this: *PackageInstaller, comptime log_level: Options.LogLevel) void { - if (comptime Environment.isWindows) { - return bun.todo(@src(), {}); - } for (this.pending_lifecycle_scripts.items) |entry| { const package_name = entry.list.package_name; while (LifecycleScriptSubprocess.alive_count.load(.Monotonic) >= this.manager.options.max_concurrent_lifecycle_scripts) { @@ -9369,10 +9322,7 @@ pub const PackageManager = struct { if (PackageManager.hasEnoughTimePassedBetweenWaitingMessages()) Output.prettyErrorln("[PackageManager] waiting for {d} tasks\n", .{PackageManager.instance.pending_tasks}); } - if (this.pending_tasks > 0) - this.sleep() - else - this.tickLifecycleScripts(); + this.sleep(); } else { this.tickLifecycleScripts(); } @@ -10132,9 +10082,6 @@ pub const PackageManager = struct { list: Lockfile.Package.Scripts.List, comptime log_level: PackageManager.Options.LogLevel, ) !void { - if (comptime Environment.isWindows) { - return bun.todo(@src(), {}); - } var any_scripts = false; for (list.items) |maybe_item| { if (maybe_item != null) { diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index ea62ce845c2419..eda35f1a779c23 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -8,131 +8,59 @@ const Environment = bun.Environment; const Output = bun.Output; const Global = bun.Global; const JSC = bun.JSC; -const WaiterThread = JSC.Subprocess.WaiterThread; +const WaiterThread = bun.spawn.WaiterThread; const Timer = std.time.Timer; +const Process = bun.spawn.Process; pub const LifecycleScriptSubprocess = struct { package_name: []const u8, scripts: Lockfile.Package.Scripts.List, current_script_index: u8 = 0, - finished_fds: u8 = 0, - - pid: std.os.pid_t = bun.invalid_fd, - - pid_poll: *Async.FilePoll, - waitpid_result: ?PosixSpawn.WaitPidResult, - stdout: OutputReader = .{}, - stderr: OutputReader = .{}, + remaining_fds: i8 = 0, + process: ?*Process = null, + stdout: OutputReader = OutputReader.init(@This()), + stderr: OutputReader = OutputReader.init(@This()), + has_called_process_exit: bool = false, manager: *PackageManager, envp: [:null]?[*:0]u8, timer: ?Timer = null, + pub usingnamespace bun.New(@This()); + pub const min_milliseconds_to_log = 500; pub var alive_count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0); - /// A "nothing" struct that lets us reuse the same pointer - /// but with a different tag for the file poll - pub const PidPollData = struct { process: LifecycleScriptSubprocess }; - - pub const OutputReader = struct { - poll: *Async.FilePoll = undefined, - buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), - is_done: bool = false, - - // This is a workaround for "Dependency loop detected" - parent: *LifecycleScriptSubprocess = undefined, + const uv = bun.windows.libuv; - pub usingnamespace bun.io.PipeReader( - @This(), - getFd, - getBuffer, - null, - registerPoll, - done, - onError, - ); + pub const OutputReader = bun.io.BufferedReader; - pub fn getFd(this: *OutputReader) bun.FileDescriptor { - return this.poll.fd; - } - - pub fn getBuffer(this: *OutputReader) *std.ArrayList(u8) { - return &this.buffer; - } - - fn finish(this: *OutputReader) void { - this.poll.flags.insert(.ignore_updates); - this.subprocess().manager.file_poll_store.hive.put(this.poll); - std.debug.assert(!this.is_done); - this.is_done = true; - } - - pub fn done(this: *OutputReader, _: []u8) void { - this.finish(); - this.subprocess().onOutputDone(); - } - - pub fn onError(this: *OutputReader, err: bun.sys.Error) void { - this.finish(); - this.subprocess().onOutputError(err); - } - - pub fn registerPoll(this: *OutputReader) void { - switch (this.poll.register(this.subprocess().manager.uws_event_loop, .readable, true)) { - .err => |err| { - Output.prettyErrorln("error: Failed to register poll for {s} script output from \"{s}\" due to error {d} {s}", .{ - this.subprocess().scriptName(), - this.subprocess().package_name, - err.errno, - @tagName(err.getErrno()), - }); - }, - .result => {}, - } - } - - pub inline fn subprocess(this: *OutputReader) *LifecycleScriptSubprocess { - return this.parent; - } - - pub fn start(this: *OutputReader) JSC.Maybe(void) { - const maybe = this.poll.register(this.subprocess().manager.uws_event_loop, .readable, true); - if (maybe != .result) { - return maybe; - } - - this.read(); + pub fn loop(this: *const LifecycleScriptSubprocess) *bun.uws.Loop { + return this.manager.event_loop.loop(); + } - return .{ - .result = {}, - }; - } - }; + pub fn eventLoop(this: *const LifecycleScriptSubprocess) *JSC.AnyEventLoop { + return &this.manager.event_loop; + } pub fn scriptName(this: *const LifecycleScriptSubprocess) []const u8 { std.debug.assert(this.current_script_index < Lockfile.Scripts.names.len); return Lockfile.Scripts.names[this.current_script_index]; } - pub fn onOutputDone(this: *LifecycleScriptSubprocess) void { - std.debug.assert(this.finished_fds < 2); - this.finished_fds += 1; + pub fn onReaderDone(this: *LifecycleScriptSubprocess) void { + std.debug.assert(this.remaining_fds > 0); + this.remaining_fds -= 1; - if (this.waitpid_result) |result| { - if (this.finished_fds == 2) { - // potential free() - this.onResult(result); - } - } + this.maybeFinished(); } - pub fn onOutputError(this: *LifecycleScriptSubprocess, err: bun.sys.Error) void { - std.debug.assert(this.finished_fds < 2); - this.finished_fds += 1; + pub fn onReaderError(this: *LifecycleScriptSubprocess, err: bun.sys.Error) void { + std.debug.assert(this.remaining_fds > 0); + this.remaining_fds -= 1; Output.prettyErrorln("error: Failed to read {s} script output from \"{s}\" due to error {d} {s}", .{ this.scriptName(), @@ -141,26 +69,34 @@ pub const LifecycleScriptSubprocess = struct { @tagName(err.getErrno()), }); Output.flush(); - if (this.waitpid_result) |result| { - if (this.finished_fds == 2) { - // potential free() - this.onResult(result); - } - } + this.maybeFinished(); } - pub fn spawnNextScript(this: *LifecycleScriptSubprocess, next_script_index: u8) !void { - if (Environment.isWindows) { - @panic("TODO"); - } + fn maybeFinished(this: *LifecycleScriptSubprocess) void { + if (!this.has_called_process_exit or this.remaining_fds != 0) + return; + const process = this.process orelse return; + this.process = null; + const status = process.status; + process.detach(); + process.deref(); + this.handleExit(status); + } + + // This is only used on the main thread. + var cwd_z_buf: bun.PathBuffer = undefined; + + pub fn spawnNextScript(this: *LifecycleScriptSubprocess, next_script_index: u8) !void { _ = alive_count.fetchAdd(1, .Monotonic); errdefer _ = alive_count.fetchSub(1, .Monotonic); const manager = this.manager; const original_script = this.scripts.items[next_script_index].?; - const cwd = this.scripts.cwd; + const cwd = bun.path.z(this.scripts.cwd, &cwd_z_buf); const env = manager.env; + this.stdout.setParent(this); + this.stderr.setParent(this); if (manager.scripts_node) |scripts_node| { manager.setNodeName( @@ -176,8 +112,7 @@ pub const LifecycleScriptSubprocess = struct { } this.current_script_index = next_script_index; - this.waitpid_result = null; - this.finished_fds = 0; + this.has_called_process_exit = false; const shell_bin = bun.CLI.RunCommand.findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell; @@ -194,378 +129,223 @@ pub const LifecycleScriptSubprocess = struct { combined_script, null, }; - // Have both stdout and stderr write to the same buffer - const fdsOut, const fdsErr = if (!this.manager.options.log_level.isVerbose()) - .{ try std.os.pipe2(0), try std.os.pipe2(0) } - else - .{ .{ 0, 0 }, .{ 0, 0 } }; - - var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK; - if (comptime Environment.isMac) { - flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; + if (Environment.isWindows) { + this.stdout.source = .{ .pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() }; + this.stderr.source = .{ .pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() }; } + const spawn_options = bun.spawn.SpawnOptions{ + .stdin = .ignore, + .stdout = if (this.manager.options.log_level.isVerbose()) + .inherit + else if (Environment.isPosix) + .buffer + else + .{ + .buffer = this.stdout.source.?.pipe, + }, + .stderr = if (this.manager.options.log_level.isVerbose()) + .inherit + else if (Environment.isPosix) + .buffer + else + .{ + .buffer = this.stderr.source.?.pipe, + }, + .cwd = cwd, - const pid = brk: { - var attr = try PosixSpawn.Attr.init(); - defer attr.deinit(); - try attr.set(@intCast(flags)); - try attr.resetSignals(); - - var actions = try PosixSpawn.Actions.init(); - defer actions.deinit(); - try actions.openZ(bun.STDIN_FD, "/dev/null", std.os.O.RDONLY, 0o664); - - if (!this.manager.options.log_level.isVerbose()) { - try actions.dup2(bun.toFD(fdsOut[1]), bun.STDOUT_FD); - try actions.dup2(bun.toFD(fdsErr[1]), bun.STDERR_FD); - } else { - if (comptime Environment.isMac) { - try actions.inherit(bun.STDOUT_FD); - try actions.inherit(bun.STDERR_FD); - } else { - try actions.dup2(bun.STDOUT_FD, bun.STDOUT_FD); - try actions.dup2(bun.STDERR_FD, bun.STDERR_FD); + .windows = if (Environment.isWindows) + .{ + .loop = JSC.EventLoopHandle.init(&manager.event_loop), } - } + else {}, + }; - try actions.chdir(cwd); + this.remaining_fds = 0; + var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), this.envp)).unwrap(); - defer { - if (!this.manager.options.log_level.isVerbose()) { - _ = bun.sys.close(bun.toFD(fdsOut[1])); - _ = bun.sys.close(bun.toFD(fdsErr[1])); - } + if (comptime Environment.isPosix) { + if (spawned.stdout) |stdout| { + this.stdout.setParent(this); + this.remaining_fds += 1; + try this.stdout.start(stdout, true).unwrap(); } - if (manager.options.log_level.isVerbose()) { - Output.prettyErrorln("[LifecycleScriptSubprocess] Spawning \"{s}\" script for package \"{s}\"\ncwd: {s}\n$ {s}", .{ - this.scriptName(), - this.package_name, - cwd, - combined_script, - }); + if (spawned.stderr) |stderr| { + this.stderr.setParent(this); + this.remaining_fds += 1; + try this.stderr.start(stderr, true).unwrap(); } - - this.timer = Timer.start() catch null; - - switch (PosixSpawn.spawnZ( - argv[0].?, - actions, - attr, - argv[0..3 :null], - this.envp, - )) { - .err => |err| { - Output.prettyErrorln("error: Failed to spawn script {s} due to error {d} {s}", .{ - this.scriptName(), - err.errno, - @tagName(err.getErrno()), - }); - Output.flush(); - return; - }, - .result => |pid| break :brk pid, + } else if (comptime Environment.isWindows) { + if (spawned.stdout == .buffer) { + this.stdout.parent = this; + this.remaining_fds += 1; + try this.stdout.startWithCurrentPipe().unwrap(); } - }; - - this.pid = pid; - - const pid_fd: std.os.fd_t = brk: { - if (!Environment.isLinux or WaiterThread.shouldUseWaiterThread()) { - break :brk pid; + if (spawned.stderr == .buffer) { + this.stderr.parent = this; + this.remaining_fds += 1; + try this.stderr.startWithCurrentPipe().unwrap(); } + } - var pidfd_flags = JSC.Subprocess.pidfdFlagsForLinux(); - - var fd = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - - while (true) { - switch (std.os.linux.getErrno(fd)) { - .SUCCESS => break :brk @intCast(fd), - .INTR => { - fd = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - continue; - }, - else => |err| { - if (err == .INVAL) { - if (pidfd_flags != 0) { - fd = std.os.linux.pidfd_open( - @intCast(pid), - 0, - ); - pidfd_flags = 0; - continue; - } - } - - if (err == .NOSYS) { - WaiterThread.setShouldUseWaiterThread(); - break :brk pid; - } - - var status: u32 = 0; - // ensure we don't leak the child process on error - _ = std.os.linux.waitpid(pid, &status, 0); - - Output.prettyErrorln("error: Failed to spawn script {s} due to error {d} {s}", .{ - this.scriptName(), - err, - @tagName(err), - }); - Output.flush(); - return; - }, - } - } - }; + const event_loop = &this.manager.event_loop; + var process = spawned.toProcess( + event_loop, + false, + ); - if (!this.manager.options.log_level.isVerbose()) { - this.stdout = .{ - .parent = this, - .poll = Async.FilePoll.initWithPackageManager(manager, bun.toFD(fdsOut[0]), .{}, &this.stdout), - }; - - this.stderr = .{ - .parent = this, - .poll = Async.FilePoll.initWithPackageManager(manager, bun.toFD(fdsErr[0]), .{}, &this.stderr), - }; - try this.stdout.start().unwrap(); - try this.stderr.start().unwrap(); + if (this.process) |proc| { + proc.detach(); + proc.deref(); } - if (WaiterThread.shouldUseWaiterThread()) { - WaiterThread.appendLifecycleScriptSubprocess(this); - } else { - this.pid_poll = Async.FilePoll.initWithPackageManager( - manager, - bun.toFD(pid_fd), - .{}, - @as(*PidPollData, @ptrCast(this)), - ); - switch (this.pid_poll.register( - this.manager.uws_event_loop, - .process, - true, - )) { - .result => {}, - .err => |err| { - // Sometimes the pid poll can fail to register if the process exits - // between posix_spawn() and pid_poll.register(), but it is unlikely. - // Any other error is unexpected here. - if (err.getErrno() != .SRCH) { - @panic("This shouldn't happen. Could not register pid poll"); - } + this.process = process; + process.setExitHandler(this); - this.onProcessUpdate(0); - }, - } - } + try process.watch(event_loop).unwrap(); } pub fn printOutput(this: *LifecycleScriptSubprocess) void { if (!this.manager.options.log_level.isVerbose()) { - if (this.stdout.buffer.items.len +| this.stderr.buffer.items.len == 0) { + if (this.stdout.buffer().items.len +| this.stderr.buffer().items.len == 0) { return; } Output.disableBuffering(); Output.flush(); - if (this.stdout.buffer.items.len > 0) { - Output.errorWriter().print("{s}\n", .{this.stdout.buffer.items}) catch {}; - this.stdout.buffer.clearAndFree(); + if (this.stdout.buffer().items.len > 0) { + Output.errorWriter().print("{s}\n", .{this.stdout.buffer().items}) catch {}; + this.stdout.buffer().clearAndFree(); } - if (this.stderr.buffer.items.len > 0) { - Output.errorWriter().print("{s}\n", .{this.stderr.buffer.items}) catch {}; - this.stderr.buffer.clearAndFree(); + if (this.stderr.buffer().items.len > 0) { + Output.errorWriter().print("{s}\n", .{this.stderr.buffer().items}) catch {}; + this.stderr.buffer().clearAndFree(); } Output.enableBuffering(); } } - pub fn onProcessUpdate(this: *LifecycleScriptSubprocess, _: i64) void { - while (true) { - switch (PosixSpawn.waitpid(this.pid, std.os.W.NOHANG)) { - .err => |err| { - Output.prettyErrorln("error: Failed to run {s} script from \"{s}\" due to error {d} {s}", .{ + fn handleExit(this: *LifecycleScriptSubprocess, status: bun.spawn.Status) void { + switch (status) { + .exited => |exit| { + const maybe_duration = if (this.timer) |*t| t.read() else null; + + if (exit.code > 0) { + this.printOutput(); + Output.prettyErrorln("error: {s} script from \"{s}\" exited with {d}", .{ this.scriptName(), this.package_name, - err.errno, - @tagName(err.getErrno()), + exit.code, }); + this.deinit(); Output.flush(); - _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .Monotonic); - _ = alive_count.fetchSub(1, .Monotonic); - return; - }, - .result => |result| { - if (result.pid != this.pid) { - continue; - } - this.onResult(result); - return; - }, - } - } - } - - /// This function may free the *LifecycleScriptSubprocess - pub fn onResult(this: *LifecycleScriptSubprocess, result: PosixSpawn.WaitPidResult) void { - _ = alive_count.fetchSub(1, .Monotonic); - if (result.pid == 0) { - Output.prettyErrorln("error: Failed to run {s} script from \"{s}\" due to error {d} {s}", .{ - this.scriptName(), - this.package_name, - 0, - "Unknown", - }); - this.deinit(); - Output.flush(); - Global.exit(1); - return; - } - if (std.os.W.IFEXITED(result.status)) { - const maybe_duration = if (this.timer) |*t| t.read() else null; - if (!this.manager.options.log_level.isVerbose()) { - std.debug.assert(this.finished_fds <= 2); - if (this.finished_fds < 2) { - this.waitpid_result = result; - return; + Global.exit(exit.code); } - } - const code = std.os.W.EXITSTATUS(result.status); - if (code > 0) { - this.printOutput(); - Output.prettyErrorln("error: {s} script from \"{s}\" exited with {any}", .{ - this.scriptName(), - this.package_name, - code, - }); - this.deinit(); - Output.flush(); - Global.exit(code); - } - - if (this.manager.scripts_node) |scripts_node| { - if (this.manager.finished_installing.load(.Monotonic)) { - scripts_node.completeOne(); - } else { - _ = @atomicRmw(usize, &scripts_node.unprotected_completed_items, .Add, 1, .Monotonic); + if (this.manager.scripts_node) |scripts_node| { + if (this.manager.finished_installing.load(.Monotonic)) { + scripts_node.completeOne(); + } else { + _ = @atomicRmw(usize, &scripts_node.unprotected_completed_items, .Add, 1, .Monotonic); + } } - } - if (maybe_duration) |nanos| { - if (nanos > min_milliseconds_to_log * std.time.ns_per_ms) { - this.manager.lifecycle_script_time_log.appendConcurrent( - this.manager.lockfile.allocator, - .{ - .package_name = this.package_name, - .script_id = this.current_script_index, - .duration = nanos, - }, - ); + if (maybe_duration) |nanos| { + if (nanos > min_milliseconds_to_log * std.time.ns_per_ms) { + this.manager.lifecycle_script_time_log.appendConcurrent( + this.manager.lockfile.allocator, + .{ + .package_name = this.package_name, + .script_id = this.current_script_index, + .duration = nanos, + }, + ); + } } - } - for (this.current_script_index + 1..Lockfile.Scripts.names.len) |new_script_index| { - if (this.scripts.items[new_script_index] != null) { - this.resetPolls(); - this.spawnNextScript(@intCast(new_script_index)) catch |err| { - Output.errGeneric("Failed to run script {s} due to error {s}", .{ - Lockfile.Scripts.names[new_script_index], - @errorName(err), - }); - Global.exit(1); - }; - return; + for (this.current_script_index + 1..Lockfile.Scripts.names.len) |new_script_index| { + if (this.scripts.items[new_script_index] != null) { + this.resetPolls(); + this.spawnNextScript(@intCast(new_script_index)) catch |err| { + Output.errGeneric("Failed to run script {s} due to error {s}", .{ + Lockfile.Scripts.names[new_script_index], + @errorName(err), + }); + Global.exit(1); + }; + return; + } } - } - // the last script finished - _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .Monotonic); - - if (!this.manager.options.log_level.isVerbose()) { - if (this.finished_fds == 2) { - this.deinit(); - } - } else { + // the last script finished + _ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .Monotonic); this.deinit(); - } + }, + .signaled => |signal| { + this.printOutput(); + Output.prettyErrorln("error: {s} script from \"{s}\" terminated by {}", .{ + this.scriptName(), + this.package_name, - return; - } - if (std.os.W.IFSIGNALED(result.status)) { - const signal = std.os.W.TERMSIG(result.status); + bun.SignalCode.from(signal).fmt(Output.enable_ansi_colors_stderr), + }); + Global.raiseIgnoringPanicHandler(@intFromEnum(signal)); - if (!this.manager.options.log_level.isVerbose()) { - if (this.finished_fds < 2) { - this.waitpid_result = result; - return; - } - } - this.printOutput(); - Output.prettyErrorln("error: {s} script from \"{s}\" terminated by {}", .{ - this.scriptName(), - this.package_name, - bun.SignalCode.from(signal).fmt(Output.enable_ansi_colors_stderr), - }); - Global.raiseIgnoringPanicHandler(signal); + return; + }, + .err => |err| { + Output.prettyErrorln("error: Failed to run {s} script from \"{s}\" due to\n{}", .{ + this.scriptName(), + this.package_name, + err, + }); + this.deinit(); + Output.flush(); + Global.exit(1); + return; + }, + else => { + Output.panic("error: Failed to run {s} script from \"{s}\" due to unexpected status\n{any}", .{ + this.scriptName(), + this.package_name, + status, + }); + }, } - if (std.os.W.IFSTOPPED(result.status)) { - const signal = std.os.W.STOPSIG(result.status); + } - if (!this.manager.options.log_level.isVerbose()) { - if (this.finished_fds < 2) { - this.waitpid_result = result; - return; - } - } - this.printOutput(); - Output.prettyErrorln("error: {s} script from \"{s}\" was stopped by {}", .{ - this.scriptName(), - this.package_name, - bun.SignalCode.from(signal).fmt(Output.enable_ansi_colors_stderr), - }); - Global.raiseIgnoringPanicHandler(signal); + /// This function may free the *LifecycleScriptSubprocess + pub fn onProcessExit(this: *LifecycleScriptSubprocess, proc: *Process, _: bun.spawn.Status, _: *const bun.spawn.Rusage) void { + if (this.process != proc) { + Output.debugWarn("[LifecycleScriptSubprocess] onProcessExit called with wrong process", .{}); + return; } - - std.debug.panic("{s} script from \"{s}\" hit unexpected state {{ .pid = {d}, .status = {d} }}", .{ - this.scriptName(), - this.package_name, - result.pid, - result.status, - }); + this.has_called_process_exit = true; + this.maybeFinished(); } pub fn resetPolls(this: *LifecycleScriptSubprocess) void { - if (!this.manager.options.log_level.isVerbose()) { - std.debug.assert(this.finished_fds == 2); - } + std.debug.assert(this.remaining_fds == 0); - const loop = this.manager.uws_event_loop; - - if (!WaiterThread.shouldUseWaiterThread()) { - _ = this.pid_poll.unregister(loop, false); - // FD is already closed + if (this.process) |process| { + this.process = null; + process.close(); + process.deref(); } } pub fn deinit(this: *LifecycleScriptSubprocess) void { this.resetPolls(); + if (!this.manager.options.log_level.isVerbose()) { - this.stdout.buffer.clearAndFree(); - this.stderr.buffer.clearAndFree(); + this.stdout.deinit(); + this.stderr.deinit(); } - this.manager.allocator.destroy(this); + + this.destroy(); } pub fn spawnPackageScripts( @@ -574,11 +354,12 @@ pub const LifecycleScriptSubprocess = struct { envp: [:null]?[*:0]u8, comptime log_level: PackageManager.Options.LogLevel, ) !void { - var lifecycle_subprocess = try manager.allocator.create(LifecycleScriptSubprocess); - lifecycle_subprocess.scripts = list; - lifecycle_subprocess.manager = manager; - lifecycle_subprocess.envp = envp; - lifecycle_subprocess.package_name = list.package_name; + var lifecycle_subprocess = LifecycleScriptSubprocess.new(.{ + .manager = manager, + .envp = envp, + .scripts = list, + .package_name = list.package_name, + }); if (comptime log_level.isVerbose()) { Output.prettyErrorln("[LifecycleScriptSubprocess] Starting scripts for \"{s}\"", .{ diff --git a/src/install/semver.zig b/src/install/semver.zig index a4325a4d313316..5754d46f3708c2 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -250,7 +250,7 @@ pub const String = extern struct { in: string, ) Pointer { if (Environment.allow_assert) { - std.debug.assert(bun.isSliceInBuffer(u8, in, buf)); + std.debug.assert(bun.isSliceInBuffer(in, buf)); } return Pointer{ diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index b36f506cfcf07a..fa3e29fbeb1d0d 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -689,7 +689,7 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { // BUF1: '\??\C:\Users\dave\project\node_modules\my-cli\src\app.js"#node #####!!!!!!!!!!' // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ^ read_ptr // BUF2: 'node "C:\Users\dave\project\node_modules\my-cli\src\app.js"!!!!!!!!!!!!!!!!!!!!' - const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - nt_object_prefix.len - shebang_arg_len_u8 + "\"".len * 2; + const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - shebang_arg_len_u8; @memcpy( buf2_u8[shebang_arg_len_u8 + 2 * "\"".len ..][0..length_of_filename_u8], buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8], diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index f034c17b33120e..3ab76f645b1540 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -1,117 +1,1084 @@ const bun = @import("root").bun; const std = @import("std"); +const uv = bun.windows.libuv; +const Source = @import("./source.zig").Source; + +const ReadState = @import("./pipes.zig").ReadState; +const FileType = @import("./pipes.zig").FileType; /// Read a blocking pipe without blocking the current thread. -pub fn PipeReader( +pub fn PosixPipeReader( comptime This: type, - // Originally this was the comptime vtable struct like the below - // But that caused a Zig compiler segfault as of 0.12.0-dev.1604+caae40c21 - comptime getFd: fn (*This) bun.FileDescriptor, - comptime getBuffer: fn (*This) *std.ArrayList(u8), - comptime onReadChunk: ?fn (*This, chunk: []u8) void, - comptime registerPoll: ?fn (*This) void, - comptime done: fn (*This, []u8) void, - comptime onError: fn (*This, bun.sys.Error) void, + comptime vtable: struct { + getFd: *const fn (*This) bun.FileDescriptor, + getBuffer: *const fn (*This) *std.ArrayList(u8), + getFileType: *const fn (*This) FileType, + onReadChunk: ?*const fn (*This, chunk: []u8, state: ReadState) void = null, + registerPoll: ?*const fn (*This) void = null, + done: *const fn (*This) void, + close: *const fn (*This) void, + onError: *const fn (*This, bun.sys.Error) void, + }, ) type { return struct { - const vtable = .{ - .getFd = getFd, - .getBuffer = getBuffer, - .onReadChunk = onReadChunk, - .registerPoll = registerPoll, - .done = done, - .onError = onError, - }; - pub fn read(this: *This) void { - const buffer = @call(bun.callmod_inline, vtable.getBuffer, .{this}); - const fd = @call(bun.callmod_inline, vtable.getFd, .{this}); - switch (bun.isReadable(fd)) { - .ready, .hup => { - readFromBlockingPipeWithoutBlocking(this, buffer, fd, 0); - }, - .not_ready => { - if (comptime vtable.registerPoll) |register| { - register(this); + const buffer = vtable.getBuffer(this); + const fd = vtable.getFd(this); + + switch (vtable.getFileType(this)) { + .nonblocking_pipe => { + readPipe(this, buffer, fd, 0, false); + return; + }, + .file => { + readFile(this, buffer, fd, 0, false); + return; + }, + .socket => { + readSocket(this, buffer, fd, 0, false); + return; + }, + .pipe => { + switch (bun.isReadable(fd)) { + .ready => { + readFromBlockingPipeWithoutBlocking(this, buffer, fd, 0, false); + }, + .hup => { + readFromBlockingPipeWithoutBlocking(this, buffer, fd, 0, true); + }, + .not_ready => { + if (comptime vtable.registerPoll) |register| { + register(this); + } + }, } }, } } - pub fn onPoll(parent: *This, size_hint: isize) void { + pub fn onPoll(parent: *This, size_hint: isize, received_hup: bool) void { const resizable_buffer = vtable.getBuffer(parent); - const fd = @call(bun.callmod_inline, vtable.getFd, .{parent}); + const fd = vtable.getFd(parent); + bun.sys.syslog("onPoll({}) = {d}", .{ fd, size_hint }); - readFromBlockingPipeWithoutBlocking(parent, resizable_buffer, fd, size_hint); + switch (vtable.getFileType(parent)) { + .nonblocking_pipe => { + readPipe(parent, resizable_buffer, fd, size_hint, received_hup); + }, + .file => { + readFile(parent, resizable_buffer, fd, size_hint, received_hup); + }, + .socket => { + readSocket(parent, resizable_buffer, fd, size_hint, received_hup); + }, + .pipe => { + readFromBlockingPipeWithoutBlocking(parent, resizable_buffer, fd, size_hint, received_hup); + }, + } } - const stack_buffer_len = 16384; + const stack_buffer_len = 64 * 1024; - fn readFromBlockingPipeWithoutBlocking(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize) void { - if (size_hint > stack_buffer_len) { - resizable_buffer.ensureUnusedCapacity(@intCast(size_hint)) catch bun.outOfMemory(); + inline fn drainChunk(parent: *This, chunk: []const u8, hasMore: ReadState) bool { + if (parent.vtable.isStreamingEnabled()) { + if (chunk.len > 0) { + return parent.vtable.onReadChunk(chunk, hasMore); + } } - const start_length: usize = resizable_buffer.items.len; + return false; + } - while (true) { - var buffer: []u8 = resizable_buffer.unusedCapacitySlice(); - var stack_buffer: [stack_buffer_len]u8 = undefined; + fn readFile(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup: bool) void { + return readWithFn(parent, resizable_buffer, fd, size_hint, received_hup, .file, bun.sys.read); + } + + fn readSocket(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup: bool) void { + return readWithFn(parent, resizable_buffer, fd, size_hint, received_hup, .socket, bun.sys.recvNonBlock); + } + + fn readPipe(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup: bool) void { + return readWithFn(parent, resizable_buffer, fd, size_hint, received_hup, .nonblocking_pipe, bun.sys.readNonblocking); + } + + fn readBlockingPipe(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup: bool) void { + return readWithFn(parent, resizable_buffer, fd, size_hint, received_hup, .pipe, bun.sys.readNonblocking); + } + + fn readWithFn(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup_: bool, comptime file_type: FileType, comptime sys_fn: *const fn (bun.FileDescriptor, []u8) JSC.Maybe(usize)) void { + _ = size_hint; // autofix + const streaming = parent.vtable.isStreamingEnabled(); + + var received_hup = received_hup_; + + if (streaming) { + const stack_buffer = parent.vtable.eventLoop().pipeReadBuffer(); + while (resizable_buffer.capacity == 0) { + const stack_buffer_cutoff = stack_buffer.len / 2; + var stack_buffer_head = stack_buffer; + while (stack_buffer_head.len > 16 * 1024) { + var buffer = stack_buffer_head; + + switch (sys_fn( + fd, + buffer, + )) { + .result => |bytes_read| { + buffer = stack_buffer_head[0..bytes_read]; + stack_buffer_head = stack_buffer_head[bytes_read..]; + + if (bytes_read == 0) { + vtable.close(parent); + if (stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len].len > 0) + _ = parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], .eof); + vtable.done(parent); + return; + } + + if (comptime file_type == .pipe) { + if (bun.Environment.isMac or !bun.C.RWFFlagSupport.isMaybeSupported()) { + switch (bun.isReadable(fd)) { + .ready => {}, + .hup => { + received_hup = true; + }, + .not_ready => { + if (received_hup) { + vtable.close(parent); + } + defer { + if (received_hup) { + vtable.done(parent); + } + } + if (stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len].len > 0) { + if (!parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], if (received_hup) .eof else .drained)) { + return; + } + } - if (buffer.len < stack_buffer_len) { - buffer = &stack_buffer; + if (!received_hup) { + if (comptime vtable.registerPoll) |register| { + register(parent); + } + } + + return; + }, + } + } + } + + if (comptime file_type != .pipe) { + // blocking pipes block a process, so we have to keep reading as much as we can + // otherwise, we do want to stream the data + if (stack_buffer_head.len < stack_buffer_cutoff) { + if (!parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], if (received_hup) .eof else .progress)) { + return; + } + stack_buffer_head = stack_buffer; + } + } + }, + .err => |err| { + if (err.isRetry()) { + if (comptime file_type == .file) { + bun.Output.debugWarn("Received EAGAIN while reading from a file. This is a bug.", .{}); + } else { + if (comptime vtable.registerPoll) |register| { + register(parent); + } + } + + if (stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len].len > 0) + _ = parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], .drained); + return; + } + + if (stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len].len > 0) + _ = parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], .progress); + vtable.onError(parent, err); + return; + }, + } + } + + if (stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len].len > 0) { + if (!parent.vtable.onReadChunk(stack_buffer[0 .. stack_buffer.len - stack_buffer_head.len], if (received_hup) .eof else .progress) and !received_hup) { + return; + } + } + + if (!parent.vtable.isStreamingEnabled()) break; } + } - switch (bun.sys.read(fd, buffer)) { + while (true) { + resizable_buffer.ensureUnusedCapacity(16 * 1024) catch bun.outOfMemory(); + var buffer: []u8 = resizable_buffer.unusedCapacitySlice(); + + switch (sys_fn(fd, buffer)) { .result => |bytes_read| { + buffer = buffer[0..bytes_read]; + resizable_buffer.items.len += bytes_read; + if (bytes_read == 0) { - vtable.done(parent, resizable_buffer.items); + vtable.close(parent); + _ = drainChunk(parent, resizable_buffer.items, .eof); + vtable.done(parent); return; } - switch (bun.isReadable(fd)) { - .ready, .hup => { - if (buffer.ptr == &stack_buffer) { - resizable_buffer.appendSlice(buffer[0..bytes_read]) catch bun.outOfMemory(); - } else { - resizable_buffer.items.len += bytes_read; - } - continue; - }, + if (comptime file_type == .pipe) { + if (bun.Environment.isMac or !bun.C.RWFFlagSupport.isMaybeSupported()) { + switch (bun.isReadable(fd)) { + .ready => {}, + .hup => { + received_hup = true; + }, + .not_ready => { + if (received_hup) { + vtable.close(parent); + } + defer { + if (received_hup) { + vtable.done(parent); + } + } - .not_ready => { - if (comptime vtable.onReadChunk) |onRead| { - if (resizable_buffer.items[start_length..].len > 0) { - onRead(parent, resizable_buffer.items[start_length..]); - } + if (parent.vtable.isStreamingEnabled()) { + defer { + resizable_buffer.clearRetainingCapacity(); + } + if (!parent.vtable.onReadChunk(resizable_buffer.items, if (received_hup) .eof else .drained) and !received_hup) { + return; + } + } - resizable_buffer.items.len = 0; + if (!received_hup) { + if (comptime vtable.registerPoll) |register| { + register(parent); + } + } - if (buffer.ptr == &stack_buffer) { - onRead(parent, buffer[0..bytes_read]); + return; + }, + } + } + } + + if (comptime file_type != .pipe) { + if (parent.vtable.isStreamingEnabled()) { + if (resizable_buffer.items.len > 128_000) { + defer { + resizable_buffer.clearRetainingCapacity(); } - } else { - if (buffer.ptr == &stack_buffer) { - resizable_buffer.appendSlice(buffer[0..bytes_read]) catch bun.outOfMemory(); - } else { - resizable_buffer.items.len += bytes_read; + if (!parent.vtable.onReadChunk(resizable_buffer.items, .progress)) { + return; } + + continue; } + } + } + }, + .err => |err| { + if (parent.vtable.isStreamingEnabled()) { + if (resizable_buffer.items.len > 0) { + _ = parent.vtable.onReadChunk(resizable_buffer.items, .drained); + resizable_buffer.clearRetainingCapacity(); + } + } + if (err.isRetry()) { + if (comptime file_type == .file) { + bun.Output.debugWarn("Received EAGAIN while reading from a file. This is a bug.", .{}); + } else { if (comptime vtable.registerPoll) |register| { register(parent); } - - return; - }, + } + return; } - }, - .err => |err| { vtable.onError(parent, err); return; }, } } } + + fn readFromBlockingPipeWithoutBlocking(parent: *This, resizable_buffer: *std.ArrayList(u8), fd: bun.FileDescriptor, size_hint: isize, received_hup: bool) void { + if (parent.vtable.isStreamingEnabled()) { + resizable_buffer.clearRetainingCapacity(); + } + + readBlockingPipe(parent, resizable_buffer, fd, size_hint, received_hup); + } + }; +} + +const PollOrFd = @import("./pipes.zig").PollOrFd; + +pub fn WindowsPipeReader( + comptime This: type, + comptime _: anytype, + comptime getBuffer: fn (*This) *std.ArrayList(u8), + comptime onReadChunk: fn (*This, chunk: []u8, ReadState) bool, + comptime registerPoll: ?fn (*This) void, + comptime done: fn (*This) void, + comptime onError: fn (*This, bun.sys.Error) void, +) type { + return struct { + fn onStreamAlloc(handle: *uv.Handle, suggested_size: usize, buf: *uv.uv_buf_t) callconv(.C) void { + var this = bun.cast(*This, handle.data); + const result = this.getReadBufferWithStableMemoryAddress(suggested_size); + buf.* = uv.uv_buf_t.init(result); + } + + fn onStreamRead(stream: *uv.uv_stream_t, nread: uv.ReturnCodeI64, buf: *const uv.uv_buf_t) callconv(.C) void { + var this = bun.cast(*This, stream.data); + + const nread_int = nread.int(); + bun.sys.syslog("onStreamRead() = {d}", .{nread_int}); + + //NOTE: pipes/tty need to call stopReading on errors (yeah) + switch (nread_int) { + 0 => { + // EAGAIN or EWOULDBLOCK or canceled (buf is not safe to access here) + return this.onRead(.{ .result = 0 }, "", .drained); + }, + uv.UV_EOF => { + _ = this.stopReading(); + // EOF (buf is not safe to access here) + return this.onRead(.{ .result = 0 }, "", .eof); + }, + else => { + if (nread.toError(.recv)) |err| { + _ = this.stopReading(); + // ERROR (buf is not safe to access here) + this.onRead(.{ .err = err }, "", .progress); + return; + } + // we got some data we can slice the buffer! + const len: usize = @intCast(nread_int); + var slice = buf.slice(); + this.onRead(.{ .result = len }, slice[0..len], .progress); + }, + } + } + + fn onFileRead(fs: *uv.fs_t) callconv(.C) void { + var this: *This = bun.cast(*This, fs.data); + const nread_int = fs.result.int(); + const continue_reading = !this.flags.is_paused; + this.flags.is_paused = true; + bun.sys.syslog("onFileRead() = {d}", .{nread_int}); + + switch (nread_int) { + // 0 actually means EOF too + 0, uv.UV_EOF => { + this.onRead(.{ .result = 0 }, "", .eof); + }, + uv.UV_ECANCELED => { + this.onRead(.{ .result = 0 }, "", .drained); + }, + else => { + if (fs.result.toError(.recv)) |err| { + this.onRead(.{ .err = err }, "", .progress); + return; + } + // continue reading + defer { + if (continue_reading) { + _ = this.startReading(); + } + } + + const len: usize = @intCast(nread_int); + // we got some data lets get the current iov + if (this.source) |source| { + if (source == .file) { + var buf = source.file.iov.slice(); + return this.onRead(.{ .result = len }, buf[0..len], .progress); + } + } + // ops we should not hit this lets fail with EPIPE + std.debug.assert(false); + return this.onRead(.{ .err = bun.sys.Error.fromCode(bun.C.E.PIPE, .read) }, "", .progress); + }, + } + } + + pub fn startReading(this: *This) bun.JSC.Maybe(void) { + if (this.flags.is_done or !this.flags.is_paused) return .{ .result = {} }; + this.flags.is_paused = false; + const source: Source = this.source orelse return .{ .err = bun.sys.Error.fromCode(bun.C.E.BADF, .read) }; + + switch (source) { + .file => |file| { + file.fs.deinit(); + source.setData(this); + const buf = this.getReadBufferWithStableMemoryAddress(64 * 1024); + file.iov = uv.uv_buf_t.init(buf); + if (uv.uv_fs_read(uv.Loop.get(), &file.fs, file.file, @ptrCast(&file.iov), 1, -1, onFileRead).toError(.write)) |err| { + return .{ .err = err }; + } + }, + else => { + if (uv.uv_read_start(source.toStream(), &onStreamAlloc, @ptrCast(&onStreamRead)).toError(.open)) |err| { + bun.windows.libuv.log("uv_read_start() = {s}", .{err.name()}); + return .{ .err = err }; + } + }, + } + + return .{ .result = {} }; + } + + pub fn stopReading(this: *This) bun.JSC.Maybe(void) { + if (this.flags.is_done or this.flags.is_paused) return .{ .result = {} }; + this.flags.is_paused = true; + const source = this.source orelse return .{ .result = {} }; + switch (source) { + .file => |file| { + file.fs.cancel(); + }, + else => { + source.toStream().readStop(); + }, + } + return .{ .result = {} }; + } + + pub fn closeImpl(this: *This, comptime callDone: bool) void { + if (this.source) |source| { + switch (source) { + .file => |file| { + file.fs.deinit(); + file.fs.data = file; + _ = uv.uv_fs_close(uv.Loop.get(), &source.file.fs, source.file.file, @ptrCast(&onFileClose)); + }, + .pipe => |pipe| { + pipe.data = pipe; + pipe.close(onPipeClose); + }, + .tty => |tty| { + tty.data = tty; + tty.close(onTTYClose); + }, + } + this.source = null; + if (comptime callDone) done(this); + } + } + + pub fn close(this: *This) void { + _ = this.stopReading(); + this.closeImpl(true); + } + + const vtable = .{ + .getBuffer = getBuffer, + .registerPoll = registerPoll, + .done = done, + .onError = onError, + }; + + fn onFileClose(handle: *uv.fs_t) callconv(.C) void { + const file = bun.cast(*Source.File, handle.data); + file.fs.deinit(); + bun.default_allocator.destroy(file); + } + + fn onPipeClose(handle: *uv.Pipe) callconv(.C) void { + const this = bun.cast(*uv.Pipe, handle.data); + bun.default_allocator.destroy(this); + } + + fn onTTYClose(handle: *uv.uv_tty_t) callconv(.C) void { + const this = bun.cast(*uv.uv_tty_t, handle.data); + bun.default_allocator.destroy(this); + } + + pub fn onRead(this: *This, amount: bun.JSC.Maybe(usize), slice: []u8, hasMore: ReadState) void { + if (amount == .err) { + onError(this, amount.err); + return; + } + + switch (hasMore) { + .eof => { + // we call report EOF and close + _ = onReadChunk(this, slice, hasMore); + close(this); + }, + .drained => { + // we call drained so we know if we should stop here + const keep_reading = onReadChunk(this, slice, hasMore); + if (!keep_reading) { + this.pause(); + } + }, + else => { + var buffer = getBuffer(this); + if (comptime bun.Environment.allow_assert) { + if (slice.len > 0 and !bun.isSliceInBuffer(slice, buffer.allocatedSlice())) { + @panic("uv_read_cb: buf is not in buffer! This is a bug in bun. Please report it."); + } + } + // move cursor foward + buffer.items.len += amount.result; + const keep_reading = onReadChunk(this, slice, hasMore); + if (!keep_reading) { + this.pause(); + } + }, + } + } + + pub fn pause(this: *This) void { + _ = this.stopReading(); + } + + pub fn unpause(this: *This) void { + _ = this.startReading(); + } + + pub fn read(this: *This) void { + // we cannot sync read pipes on Windows so we just check if we are paused to resume the reading + this.unpause(); + } }; } + +pub const PipeReader = if (bun.Environment.isWindows) WindowsPipeReader else PosixPipeReader; +const Async = bun.Async; + +// This is a runtime type instead of comptime due to bugs in Zig. +// https://github.com/ziglang/zig/issues/18664 +const BufferedReaderVTable = struct { + parent: *anyopaque = undefined, + fns: *const Fn = undefined, + + pub fn init(comptime Type: type) BufferedReaderVTable { + return .{ + .fns = Fn.init(Type), + }; + } + + pub const Fn = struct { + onReadChunk: ?*const fn (*anyopaque, chunk: []const u8, hasMore: ReadState) bool = null, + onReaderDone: *const fn (*anyopaque) void, + onReaderError: *const fn (*anyopaque, bun.sys.Error) void, + loop: *const fn (*anyopaque) *Async.Loop, + eventLoop: *const fn (*anyopaque) JSC.EventLoopHandle, + + pub fn init(comptime Type: type) *const BufferedReaderVTable.Fn { + const loop_fn = &struct { + pub fn loop_fn(this: *anyopaque) *Async.Loop { + return Type.loop(@alignCast(@ptrCast(this))); + } + }.loop_fn; + + const eventLoop_fn = &struct { + pub fn eventLoop_fn(this: *anyopaque) JSC.EventLoopHandle { + return JSC.EventLoopHandle.init(Type.eventLoop(@alignCast(@ptrCast(this)))); + } + }.eventLoop_fn; + return comptime &BufferedReaderVTable.Fn{ + .onReadChunk = if (@hasDecl(Type, "onReadChunk")) @ptrCast(&Type.onReadChunk) else null, + .onReaderDone = @ptrCast(&Type.onReaderDone), + .onReaderError = @ptrCast(&Type.onReaderError), + .eventLoop = eventLoop_fn, + .loop = loop_fn, + }; + } + }; + + pub fn eventLoop(this: @This()) JSC.EventLoopHandle { + return this.fns.eventLoop(this.parent); + } + + pub fn loop(this: @This()) *Async.Loop { + return this.fns.loop(this.parent); + } + + pub fn isStreamingEnabled(this: @This()) bool { + return this.fns.onReadChunk != null; + } + + /// When the reader has read a chunk of data + /// and hasMore is true, it means that there might be more data to read. + /// + /// Returning false prevents the reader from reading more data. + pub fn onReadChunk(this: @This(), chunk: []const u8, hasMore: ReadState) bool { + return this.fns.onReadChunk.?(this.parent, chunk, hasMore); + } + + pub fn onReaderDone(this: @This()) void { + this.fns.onReaderDone(this.parent); + } + + pub fn onReaderError(this: @This(), err: bun.sys.Error) void { + this.fns.onReaderError(this.parent, err); + } +}; + +const PosixBufferedReader = struct { + handle: PollOrFd = .{ .closed = {} }, + _buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), + vtable: BufferedReaderVTable, + flags: Flags = .{}, + + const Flags = packed struct { + is_done: bool = false, + pollable: bool = false, + nonblocking: bool = false, + socket: bool = false, + received_eof: bool = false, + closed_without_reporting: bool = false, + close_handle: bool = true, + }; + + pub fn init(comptime Type: type) PosixBufferedReader { + return .{ + .vtable = BufferedReaderVTable.init(Type), + }; + } + + pub fn updateRef(this: *const PosixBufferedReader, value: bool) void { + const poll = this.handle.getPoll() orelse return; + poll.setKeepingProcessAlive(this.vtable.eventLoop(), value); + } + + pub inline fn isDone(this: *const PosixBufferedReader) bool { + return this.flags.is_done or this.flags.received_eof or this.flags.closed_without_reporting; + } + + pub fn from(to: *@This(), other: *PosixBufferedReader, parent_: *anyopaque) void { + to.* = .{ + .handle = other.handle, + ._buffer = other.buffer().*, + .flags = other.flags, + .vtable = .{ + .fns = to.vtable.fns, + .parent = parent_, + }, + }; + other.buffer().* = std.ArrayList(u8).init(bun.default_allocator); + other.flags.is_done = true; + other.handle = .{ .closed = {} }; + to.handle.setOwner(to); + + // note: the caller is supposed to drain the buffer themselves + // doing it here automatically makes it very easy to end up reading from the same buffer multiple times. + } + + pub fn setParent(this: *PosixBufferedReader, parent_: *anyopaque) void { + this.vtable.parent = parent_; + this.handle.setOwner(this); + } + + pub usingnamespace PosixPipeReader(@This(), .{ + .getFd = @ptrCast(&getFd), + .getBuffer = @ptrCast(&buffer), + .onReadChunk = @ptrCast(&_onReadChunk), + .registerPoll = @ptrCast(®isterPoll), + .done = @ptrCast(&done), + .close = @ptrCast(&closeWithoutReporting), + .onError = @ptrCast(&onError), + .getFileType = @ptrCast(&getFileType), + }); + + fn getFileType(this: *const PosixBufferedReader) FileType { + const flags = this.flags; + if (flags.socket) { + return .socket; + } + + if (flags.pollable) { + if (flags.nonblocking) { + return .nonblocking_pipe; + } + + return .pipe; + } + + return .file; + } + + pub fn close(this: *PosixBufferedReader) void { + this.closeHandle(); + } + + fn closeWithoutReporting(this: *PosixBufferedReader) void { + if (this.getFd() != bun.invalid_fd) { + std.debug.assert(!this.flags.closed_without_reporting); + this.flags.closed_without_reporting = true; + if (this.flags.close_handle) this.handle.close(this, {}); + } + } + + fn _onReadChunk(this: *PosixBufferedReader, chunk: []u8, hasMore: ReadState) bool { + if (hasMore == .eof) { + this.flags.received_eof = true; + } + + return this.vtable.onReadChunk(chunk, hasMore); + } + + pub fn getFd(this: *PosixBufferedReader) bun.FileDescriptor { + return this.handle.getFd(); + } + + // No-op on posix. + pub fn pause(this: *PosixBufferedReader) void { + _ = this; // autofix + + } + + pub fn takeBuffer(this: *PosixBufferedReader) std.ArrayList(u8) { + const out = this._buffer; + this._buffer = std.ArrayList(u8).init(out.allocator); + return out; + } + + pub fn buffer(this: *PosixBufferedReader) *std.ArrayList(u8) { + return &@as(*PosixBufferedReader, @alignCast(@ptrCast(this)))._buffer; + } + + pub fn disableKeepingProcessAlive(this: *@This(), event_loop_ctx: anytype) void { + _ = event_loop_ctx; // autofix + this.updateRef(false); + } + + pub fn enableKeepingProcessAlive(this: *@This(), event_loop_ctx: anytype) void { + _ = event_loop_ctx; // autofix + this.updateRef(true); + } + + fn finish(this: *PosixBufferedReader) void { + if (this.handle != .closed or this.flags.closed_without_reporting) { + if (this.flags.close_handle) this.closeHandle(); + return; + } + + std.debug.assert(!this.flags.is_done); + this.flags.is_done = true; + } + + fn closeHandle(this: *PosixBufferedReader) void { + if (this.flags.closed_without_reporting) { + this.flags.closed_without_reporting = false; + this.done(); + return; + } + + if (this.flags.close_handle) this.handle.close(this, done); + } + + pub fn done(this: *PosixBufferedReader) void { + if (this.handle != .closed and this.flags.close_handle) { + this.closeHandle(); + return; + } else if (this.flags.closed_without_reporting) { + this.flags.closed_without_reporting = false; + } + this.finish(); + this.vtable.onReaderDone(); + } + + pub fn deinit(this: *PosixBufferedReader) void { + this.buffer().clearAndFree(); + this.closeHandle(); + } + + pub fn onError(this: *PosixBufferedReader, err: bun.sys.Error) void { + this.vtable.onReaderError(err); + } + + pub fn registerPoll(this: *PosixBufferedReader) void { + const poll = this.handle.getPoll() orelse brk: { + if (this.handle == .fd and this.flags.pollable) { + this.handle = .{ .poll = Async.FilePoll.init(this.eventLoop(), this.handle.fd, .{}, @This(), this) }; + break :brk this.handle.poll; + } + + return; + }; + poll.owner.set(this); + + if (!poll.flags.contains(.was_ever_registered)) + poll.enableKeepingProcessAlive(this.eventLoop()); + + switch (poll.registerWithFd(this.loop(), .readable, .dispatch, poll.fd)) { + .err => |err| { + this.onError(err); + }, + .result => {}, + } + } + + pub fn start(this: *PosixBufferedReader, fd: bun.FileDescriptor, is_pollable: bool) bun.JSC.Maybe(void) { + if (!is_pollable) { + this.buffer().clearRetainingCapacity(); + this.flags.is_done = false; + this.handle.close(null, {}); + this.handle = .{ .fd = fd }; + return .{ .result = {} }; + } + this.flags.pollable = true; + if (this.getFd() != fd) { + this.handle = .{ .fd = fd }; + } + this.registerPoll(); + + return .{ + .result = {}, + }; + } + + // Exists for consistentcy with Windows. + pub fn hasPendingRead(this: *const PosixBufferedReader) bool { + return this.handle == .poll and this.handle.poll.isRegistered(); + } + + pub fn watch(this: *PosixBufferedReader) void { + if (this.flags.pollable) { + this.registerPoll(); + } + } + + pub fn hasPendingActivity(this: *const PosixBufferedReader) bool { + return switch (this.handle) { + .poll => |poll| poll.isActive(), + .fd => true, + else => false, + }; + } + + pub fn loop(this: *const PosixBufferedReader) *Async.Loop { + return this.vtable.loop(); + } + + pub fn eventLoop(this: *const PosixBufferedReader) JSC.EventLoopHandle { + return this.vtable.eventLoop(); + } + + comptime { + bun.meta.banFieldType(@This(), bool); // put them in flags instead. + } +}; + +const JSC = bun.JSC; + +const WindowsOutputReaderVTable = struct { + onReaderDone: *const fn (*anyopaque) void, + onReaderError: *const fn (*anyopaque, bun.sys.Error) void, + onReadChunk: ?*const fn ( + *anyopaque, + chunk: []const u8, + hasMore: ReadState, + ) bool = null, +}; + +pub const WindowsBufferedReader = struct { + /// The pointer to this pipe must be stable. + /// It cannot change because we don't know what libuv will do with it. + source: ?Source = null, + _buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), + // for compatibility with Linux + flags: Flags = .{}, + + parent: *anyopaque = undefined, + vtable: WindowsOutputReaderVTable = undefined, + ref_count: u32 = 1, + pub usingnamespace bun.NewRefCounted(@This(), deinit); + + const WindowsOutputReader = @This(); + + const Flags = packed struct { + is_done: bool = false, + pollable: bool = false, + nonblocking: bool = false, + received_eof: bool = false, + closed_without_reporting: bool = false, + close_handle: bool = true, + + is_paused: bool = true, + has_inflight_read: bool = false, + }; + + pub fn init(comptime Type: type) WindowsOutputReader { + return .{ + .vtable = .{ + .onReadChunk = if (@hasDecl(Type, "onReadChunk")) @ptrCast(&Type.onReadChunk) else null, + .onReaderDone = @ptrCast(&Type.onReaderDone), + .onReaderError = @ptrCast(&Type.onReaderError), + }, + }; + } + + pub inline fn isDone(this: *WindowsOutputReader) bool { + return this.flags.is_done or this.flags.received_eof or this.flags.closed_without_reporting; + } + + pub fn from(to: *WindowsOutputReader, other: anytype, parent: anytype) void { + std.debug.assert(other.source != null and to.source == null); + to.* = .{ + .vtable = to.vtable, + .flags = other.flags, + ._buffer = other.buffer().*, + .source = other.source, + }; + other.flags.is_done = true; + other.source = null; + to.setParent(parent); + } + + pub fn getFd(this: *const WindowsOutputReader) bun.FileDescriptor { + const source = this.source orelse return bun.invalid_fd; + return source.getFd(); + } + + pub fn watch(_: *WindowsOutputReader) void { + // No-op on windows. + } + + pub fn setParent(this: *WindowsOutputReader, parent: anytype) void { + this.parent = parent; + if (!this.flags.is_done) { + if (this.source) |source| { + source.setData(this); + } + } + } + + pub fn updateRef(this: *WindowsOutputReader, value: bool) void { + if (this.source) |source| { + if (value) { + source.ref(); + } else { + source.unref(); + } + } + } + + pub fn enableKeepingProcessAlive(this: *WindowsOutputReader, _: anytype) void { + this.updateRef(true); + } + + pub fn disableKeepingProcessAlive(this: *WindowsOutputReader, _: anytype) void { + this.updateRef(false); + } + + pub usingnamespace WindowsPipeReader( + @This(), + {}, + buffer, + _onReadChunk, + null, + done, + onError, + ); + + pub fn takeBuffer(this: *WindowsOutputReader) std.ArrayList(u8) { + const out = this._buffer; + this._buffer = std.ArrayList(u8).init(out.allocator); + return out; + } + + pub fn buffer(this: *WindowsOutputReader) *std.ArrayList(u8) { + return &this._buffer; + } + + pub fn hasPendingActivity(this: *const WindowsOutputReader) bool { + const source = this.source orelse return false; + return source.isActive(); + } + + pub fn hasPendingRead(this: *const WindowsOutputReader) bool { + return this.flags.has_inflight_read; + } + + fn _onReadChunk(this: *WindowsOutputReader, buf: []u8, hasMore: ReadState) bool { + this.flags.has_inflight_read = false; + if (hasMore == .eof) { + this.flags.received_eof = true; + } + + const onReadChunkFn = this.vtable.onReadChunk orelse return true; + return onReadChunkFn(this.parent, buf, hasMore); + } + + fn finish(this: *WindowsOutputReader) void { + this.flags.has_inflight_read = false; + this.flags.is_done = true; + } + + pub fn done(this: *WindowsOutputReader) void { + std.debug.assert(if (this.source) |source| source.isClosed() else true); + + this.finish(); + + this.vtable.onReaderDone(this.parent); + } + + pub fn onError(this: *WindowsOutputReader, err: bun.sys.Error) void { + this.finish(); + this.vtable.onReaderError(this.parent, err); + } + + pub fn getReadBufferWithStableMemoryAddress(this: *WindowsOutputReader, suggested_size: usize) []u8 { + this.flags.has_inflight_read = true; + this._buffer.ensureUnusedCapacity(suggested_size) catch bun.outOfMemory(); + const res = this._buffer.allocatedSlice()[this._buffer.items.len..]; + return res; + } + + pub fn startWithCurrentPipe(this: *WindowsOutputReader) bun.JSC.Maybe(void) { + std.debug.assert(this.source != null); + this.buffer().clearRetainingCapacity(); + this.flags.is_done = false; + this.unpause(); + return .{ .result = {} }; + } + + pub fn startWithPipe(this: *WindowsOutputReader, pipe: *uv.Pipe) bun.JSC.Maybe(void) { + std.debug.assert(this.source == null); + this.source = .{ .pipe = pipe }; + return this.startWithCurrentPipe(); + } + + pub fn start(this: *WindowsOutputReader, fd: bun.FileDescriptor, _: bool) bun.JSC.Maybe(void) { + std.debug.assert(this.source == null); + const source = switch (Source.open(uv.Loop.get(), fd)) { + .err => |err| return .{ .err = err }, + .result => |source| source, + }; + source.setData(this); + this.source = source; + return this.startWithCurrentPipe(); + } + + pub fn deinit(this: *WindowsOutputReader) void { + this.buffer().deinit(); + const source = this.source orelse return; + if (!source.isClosed()) { + // closeImpl will take care of freeing the source + this.closeImpl(false); + } + this.source = null; + } + + comptime { + bun.meta.banFieldType(WindowsOutputReader, bool); // Don't increase the size of the struct. Put them in flags instead. + } +}; + +pub const BufferedReader = if (bun.Environment.isPosix) + PosixBufferedReader +else if (bun.Environment.isWindows) + WindowsBufferedReader +else + @compileError("Unsupported platform"); diff --git a/src/io/PipeWriter.zig b/src/io/PipeWriter.zig new file mode 100644 index 00000000000000..4805d165fb987a --- /dev/null +++ b/src/io/PipeWriter.zig @@ -0,0 +1,1288 @@ +const bun = @import("root").bun; +const std = @import("std"); +const Async = bun.Async; +const JSC = bun.JSC; +const uv = bun.windows.libuv; +const Source = @import("./source.zig").Source; + +const log = bun.Output.scoped(.PipeWriter, false); +const FileType = @import("./pipes.zig").FileType; + +pub const WriteResult = union(enum) { + done: usize, + wrote: usize, + pending: usize, + err: bun.sys.Error, +}; + +pub const WriteStatus = enum { + end_of_file, + drained, + pending, +}; + +pub fn PosixPipeWriter( + comptime This: type, + // Originally this was the comptime vtable struct like the below + // But that caused a Zig compiler segfault as of 0.12.0-dev.1604+caae40c21 + comptime getFd: fn (*This) bun.FileDescriptor, + comptime getBuffer: fn (*This) []const u8, + comptime onWrite: fn (*This, written: usize, status: WriteStatus) void, + comptime registerPoll: ?fn (*This) void, + comptime onError: fn (*This, bun.sys.Error) void, + comptime onWritable: fn (*This) void, + comptime getFileType: *const fn (*This) FileType, +) type { + _ = onWritable; // autofix + return struct { + pub fn _tryWrite(this: *This, buf_: []const u8) WriteResult { + return switch (getFileType(this)) { + inline else => |ft| return _tryWriteWithWriteFn(this, buf_, comptime writeToFileType(ft)), + }; + } + + fn _tryWriteWithWriteFn(this: *This, buf_: []const u8, comptime write_fn: *const fn (bun.FileDescriptor, []const u8) JSC.Maybe(usize)) WriteResult { + const fd = getFd(this); + var buf = buf_; + + while (buf.len > 0) { + switch (write_fn(fd, buf)) { + .err => |err| { + if (err.isRetry()) { + return .{ .pending = buf_.len - buf.len }; + } + + if (err.getErrno() == .PIPE) { + return .{ .done = buf_.len - buf.len }; + } + + return .{ .err = err }; + }, + + .result => |wrote| { + if (wrote == 0) { + return .{ .done = buf_.len - buf.len }; + } + + buf = buf[wrote..]; + }, + } + } + + return .{ .wrote = buf_.len - buf.len }; + } + + fn writeToFileType(comptime file_type: FileType) *const (fn (bun.FileDescriptor, []const u8) JSC.Maybe(usize)) { + comptime return switch (file_type) { + .nonblocking_pipe, .file => &bun.sys.write, + .pipe => &writeToBlockingPipe, + .socket => &bun.sys.sendNonBlock, + }; + } + + fn writeToBlockingPipe(fd: bun.FileDescriptor, buf: []const u8) JSC.Maybe(usize) { + if (comptime bun.Environment.isLinux) { + if (bun.C.linux.RWFFlagSupport.isMaybeSupported()) { + return bun.sys.writeNonblocking(fd, buf); + } + } + + switch (bun.isWritable(fd)) { + .ready, .hup => return bun.sys.write(fd, buf), + .not_ready => return JSC.Maybe(usize){ .err = bun.sys.Error.retry }, + } + } + + pub fn onPoll(parent: *This, size_hint: isize, received_hup: bool) void { + const buffer = getBuffer(parent); + log("onPoll({})", .{buffer.len}); + if (buffer.len == 0 and !received_hup) { + return; + } + + switch (drainBufferedData( + parent, + buffer, + if (size_hint > 0 and getFileType(parent).isBlocking()) @intCast(size_hint) else std.math.maxInt(usize), + received_hup, + )) { + .pending => |wrote| { + if (wrote > 0) + onWrite(parent, wrote, .pending); + + if (comptime registerPoll) |register| { + register(parent); + } + }, + .wrote => |amt| { + onWrite(parent, amt, .drained); + if (@hasDecl(This, "auto_poll")) { + if (!This.auto_poll) return; + } + if (getBuffer(parent).len > 0) { + if (comptime registerPoll) |register| { + register(parent); + } + } + }, + .err => |err| { + onError(parent, err); + }, + .done => |amt| { + onWrite(parent, amt, .end_of_file); + }, + } + } + + pub fn drainBufferedData(parent: *This, input_buffer: []const u8, max_write_size: usize, received_hup: bool) WriteResult { + _ = received_hup; // autofix + var buf = input_buffer; + buf = if (max_write_size < buf.len and max_write_size > 0) buf[0..max_write_size] else buf; + const original_buf = buf; + + while (buf.len > 0) { + const attempt = _tryWrite(parent, buf); + switch (attempt) { + .pending => |pending| { + return .{ .pending = pending + (original_buf.len - buf.len) }; + }, + .wrote => |amt| { + buf = buf[amt..]; + }, + .err => |err| { + const wrote = original_buf.len - buf.len; + if (err.isRetry()) { + return .{ .pending = wrote }; + } + + if (wrote > 0) { + onError(parent, err); + return .{ .wrote = wrote }; + } else { + return .{ .err = err }; + } + }, + .done => |amt| { + buf = buf[amt..]; + const wrote = original_buf.len - buf.len; + + return .{ .done = wrote }; + }, + } + } + + const wrote = original_buf.len - buf.len; + return .{ .wrote = wrote }; + } + }; +} + +const PollOrFd = @import("./pipes.zig").PollOrFd; + +pub fn PosixBufferedWriter( + comptime Parent: type, + comptime onWrite: *const fn (*Parent, amount: usize, status: WriteStatus) void, + comptime onError: *const fn (*Parent, bun.sys.Error) void, + comptime onClose: ?*const fn (*Parent) void, + comptime getBuffer: *const fn (*Parent) []const u8, + comptime onWritable: ?*const fn (*Parent) void, +) type { + return struct { + handle: PollOrFd = .{ .closed = {} }, + parent: *Parent = undefined, + is_done: bool = false, + pollable: bool = false, + closed_without_reporting: bool = false, + close_fd: bool = true, + + const PosixWriter = @This(); + + pub const auto_poll = if (@hasDecl(Parent, "auto_poll")) Parent.auto_poll else true; + + pub fn createPoll(this: *@This(), fd: bun.FileDescriptor) *Async.FilePoll { + return Async.FilePoll.init(@as(*Parent, @ptrCast(this.parent)).eventLoop(), fd, .{}, PosixWriter, this); + } + + pub fn getPoll(this: *const @This()) ?*Async.FilePoll { + return this.handle.getPoll(); + } + + pub fn getFileType(this: *const @This()) FileType { + const poll = getPoll(this) orelse return FileType.file; + + return poll.fileType(); + } + + pub fn getFd(this: *const PosixWriter) bun.FileDescriptor { + return this.handle.getFd(); + } + + fn _onError( + this: *PosixWriter, + err: bun.sys.Error, + ) void { + std.debug.assert(!err.isRetry()); + + onError(this.parent, err); + + this.close(); + } + + fn _onWrite( + this: *PosixWriter, + written: usize, + status: WriteStatus, + ) void { + const was_done = this.is_done == true; + const parent = this.parent; + + if (status == .end_of_file and !was_done) { + this.closeWithoutReporting(); + } + + onWrite(parent, written, status); + if (status == .end_of_file and !was_done) { + this.close(); + } + } + + fn _onWritable(this: *PosixWriter) void { + if (this.is_done) { + return; + } + + if (onWritable) |cb| { + cb(this.parent); + } + } + + pub fn registerPoll(this: *PosixWriter) void { + var poll = this.getPoll() orelse return; + switch (poll.registerWithFd(bun.uws.Loop.get(), .writable, .dispatch, poll.fd)) { + .err => |err| { + onError(this.parent, err); + }, + .result => {}, + } + } + + pub const tryWrite = @This()._tryWrite; + + pub fn hasRef(this: *PosixWriter) bool { + if (this.is_done) { + return false; + } + + const poll = this.getPoll() orelse return false; + return poll.canEnableKeepingProcessAlive(); + } + + pub fn enableKeepingProcessAlive(this: *PosixWriter, event_loop: anytype) void { + this.updateRef(event_loop, true); + } + + pub fn disableKeepingProcessAlive(this: *PosixWriter, event_loop: anytype) void { + this.updateRef(event_loop, false); + } + + fn getBufferInternal(this: *PosixWriter) []const u8 { + return getBuffer(this.parent); + } + + pub usingnamespace PosixPipeWriter(@This(), getFd, getBufferInternal, _onWrite, registerPoll, _onError, _onWritable, getFileType); + + pub fn end(this: *PosixWriter) void { + if (this.is_done) { + return; + } + + this.is_done = true; + this.close(); + } + + fn closeWithoutReporting(this: *PosixWriter) void { + if (this.getFd() != bun.invalid_fd) { + std.debug.assert(!this.closed_without_reporting); + this.closed_without_reporting = true; + if (this.close_fd) this.handle.close(null, {}); + } + } + + pub fn close(this: *PosixWriter) void { + if (onClose) |closer| { + if (this.closed_without_reporting) { + this.closed_without_reporting = false; + closer(this.parent); + } else { + this.handle.closeImpl(this.parent, closer, this.close_fd); + } + } + } + + pub fn updateRef(this: *const PosixWriter, event_loop: anytype, value: bool) void { + const poll = this.getPoll() orelse return; + poll.setKeepingProcessAlive(event_loop, value); + } + + pub fn setParent(this: *PosixWriter, parent: *Parent) void { + this.parent = parent; + this.handle.setOwner(this); + } + + pub fn write(this: *PosixWriter) void { + this.onPoll(0, false); + } + + pub fn watch(this: *PosixWriter) void { + if (this.pollable) { + if (this.handle == .fd) { + this.handle = .{ .poll = this.createPoll(this.getFd()) }; + } + + this.registerPoll(); + } + } + + pub fn start(this: *PosixWriter, fd: bun.FileDescriptor, pollable: bool) JSC.Maybe(void) { + this.pollable = pollable; + if (!pollable) { + std.debug.assert(this.handle != .poll); + this.handle = .{ .fd = fd }; + return JSC.Maybe(void){ .result = {} }; + } + var poll = this.getPoll() orelse brk: { + this.handle = .{ .poll = this.createPoll(fd) }; + break :brk this.handle.poll; + }; + const loop = @as(*Parent, @ptrCast(this.parent)).eventLoop().loop(); + + switch (poll.registerWithFd(loop, .writable, .dispatch, fd)) { + .err => |err| { + return JSC.Maybe(void){ .err = err }; + }, + .result => { + this.enableKeepingProcessAlive(@as(*Parent, @ptrCast(this.parent)).eventLoop()); + }, + } + + return JSC.Maybe(void){ .result = {} }; + } + }; +} + +pub fn PosixStreamingWriter( + comptime Parent: type, + comptime onWrite: fn (*Parent, amount: usize, status: WriteStatus) void, + comptime onError: fn (*Parent, bun.sys.Error) void, + comptime onReady: ?fn (*Parent) void, + comptime onClose: fn (*Parent) void, +) type { + return struct { + // TODO: replace buffer + head for StreamBuffer + buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), + handle: PollOrFd = .{ .closed = {} }, + parent: *Parent = undefined, + head: usize = 0, + is_done: bool = false, + closed_without_reporting: bool = false, + + // TODO: + chunk_size: usize = 0, + + pub fn getPoll(this: *@This()) ?*Async.FilePoll { + return this.handle.getPoll(); + } + + pub fn getFd(this: *PosixWriter) bun.FileDescriptor { + return this.handle.getFd(); + } + + pub fn getFileType(this: *PosixWriter) FileType { + const poll = this.getPoll() orelse return FileType.file; + + return poll.fileType(); + } + + const PosixWriter = @This(); + + pub fn getBuffer(this: *PosixWriter) []const u8 { + return this.buffer.items[this.head..]; + } + + fn _onError( + this: *PosixWriter, + err: bun.sys.Error, + ) void { + std.debug.assert(!err.isRetry()); + + this.closeWithoutReporting(); + this.is_done = true; + + onError(@alignCast(@ptrCast(this.parent)), err); + this.close(); + } + + fn _onWrite( + this: *PosixWriter, + written: usize, + status: WriteStatus, + ) void { + this.head += written; + + if (status == .end_of_file and !this.is_done) { + this.closeWithoutReporting(); + } + + if (this.buffer.items.len == this.head) { + if (this.buffer.capacity > 1024 * 1024 and status != .end_of_file) { + this.buffer.clearAndFree(); + } else { + this.buffer.clearRetainingCapacity(); + } + this.head = 0; + } + + onWrite(@ptrCast(this.parent), written, status); + } + + pub fn setParent(this: *PosixWriter, parent: *Parent) void { + this.parent = parent; + this.handle.setOwner(this); + } + + fn _onWritable(this: *PosixWriter) void { + if (this.is_done or this.closed_without_reporting) { + return; + } + + this.head = 0; + if (onReady) |cb| { + cb(@ptrCast(this.parent)); + } + } + + pub fn hasPendingData(this: *const PosixWriter) bool { + return this.buffer.items.len > 0; + } + + fn closeWithoutReporting(this: *PosixWriter) void { + if (this.getFd() != bun.invalid_fd) { + std.debug.assert(!this.closed_without_reporting); + this.closed_without_reporting = true; + this.handle.close(null, {}); + } + } + + fn registerPoll(this: *PosixWriter) void { + const poll = this.getPoll() orelse return; + switch (poll.registerWithFd(@as(*Parent, @ptrCast(this.parent)).loop(), .writable, .dispatch, poll.fd)) { + .err => |err| { + onError(this.parent, err); + this.close(); + }, + .result => {}, + } + } + + pub fn tryWrite(this: *PosixWriter, buf: []const u8) WriteResult { + if (this.is_done or this.closed_without_reporting) { + return .{ .done = 0 }; + } + + if (this.buffer.items.len > 0) { + this.buffer.appendSlice(buf) catch { + return .{ .err = bun.sys.Error.oom }; + }; + + return .{ .pending = 0 }; + } + + return @This()._tryWrite(this, buf); + } + + pub fn writeUTF16(this: *PosixWriter, buf: []const u16) WriteResult { + if (this.is_done or this.closed_without_reporting) { + return .{ .done = 0 }; + } + + const had_buffered_data = this.buffer.items.len > 0; + { + var byte_list = bun.ByteList.fromList(this.buffer); + defer this.buffer = byte_list.listManaged(bun.default_allocator); + + _ = byte_list.writeUTF16(bun.default_allocator, buf) catch { + return .{ .err = bun.sys.Error.oom }; + }; + } + + if (had_buffered_data) { + return .{ .pending = 0 }; + } + + return this._tryWriteNewlyBufferedData(); + } + + pub fn writeLatin1(this: *PosixWriter, buf: []const u8) WriteResult { + if (this.is_done or this.closed_without_reporting) { + return .{ .done = 0 }; + } + + if (bun.strings.isAllASCII(buf)) { + return this.write(buf); + } + + const had_buffered_data = this.buffer.items.len > 0; + { + var byte_list = bun.ByteList.fromList(this.buffer); + defer this.buffer = byte_list.listManaged(bun.default_allocator); + + _ = byte_list.writeLatin1(bun.default_allocator, buf) catch { + return .{ .err = bun.sys.Error.oom }; + }; + } + + if (had_buffered_data) { + return .{ .pending = 0 }; + } + + return this._tryWriteNewlyBufferedData(); + } + + fn _tryWriteNewlyBufferedData(this: *PosixWriter) WriteResult { + std.debug.assert(!this.is_done); + + switch (@This()._tryWrite(this, this.buffer.items)) { + .wrote => |amt| { + if (amt == this.buffer.items.len) { + this.buffer.clearRetainingCapacity(); + } else { + this.head = amt; + } + return .{ .wrote = amt }; + }, + .done => |amt| { + this.buffer.clearRetainingCapacity(); + + return .{ .done = amt }; + }, + else => |r| return r, + } + } + + pub fn write(this: *PosixWriter, buf: []const u8) WriteResult { + if (this.is_done or this.closed_without_reporting) { + return .{ .done = 0 }; + } + + if (this.buffer.items.len + buf.len < this.chunk_size) { + this.buffer.appendSlice(buf) catch { + return .{ .err = bun.sys.Error.oom }; + }; + + return .{ .pending = 0 }; + } + + const rc = @This()._tryWrite(this, buf); + this.head = 0; + switch (rc) { + .pending => |amt| { + this.buffer.appendSlice(buf[amt..]) catch { + return .{ .err = bun.sys.Error.oom }; + }; + + onWrite(this.parent, amt, .pending); + + registerPoll(this); + }, + .wrote => |amt| { + if (amt < buf.len) { + this.buffer.appendSlice(buf[amt..]) catch { + return .{ .err = bun.sys.Error.oom }; + }; + onWrite(this.parent, amt, .pending); + } else { + this.buffer.clearRetainingCapacity(); + onWrite(this.parent, amt, .drained); + } + }, + .done => |amt| { + this.buffer.clearRetainingCapacity(); + onWrite(this.parent, amt, .end_of_file); + return .{ .done = amt }; + }, + else => {}, + } + + return rc; + } + + pub usingnamespace PosixPipeWriter(@This(), getFd, getBuffer, _onWrite, registerPoll, _onError, _onWritable, getFileType); + + pub fn flush(this: *PosixWriter) WriteResult { + if (this.closed_without_reporting or this.is_done) { + return .{ .done = 0 }; + } + + const buffer = this.buffer.items; + if (buffer.len == 0) { + return .{ .wrote = 0 }; + } + + const rc = this.drainBufferedData(buffer, std.math.maxInt(usize), brk: { + if (this.getPoll()) |poll| { + break :brk poll.flags.contains(.hup); + } + + break :brk false; + }); + // update head + switch (rc) { + .pending => |written| { + this.head += written; + }, + .wrote => |written| { + this.head += written; + }, + .done => |written| { + this.head += written; + }, + else => {}, + } + return rc; + } + + pub fn deinit(this: *PosixWriter) void { + this.buffer.clearAndFree(); + this.close(); + } + + pub fn hasRef(this: *PosixWriter) bool { + const poll = this.getPoll() orelse return false; + return !this.is_done and poll.canEnableKeepingProcessAlive(); + } + + pub fn enableKeepingProcessAlive(this: *PosixWriter, event_loop: JSC.EventLoopHandle) void { + if (this.is_done) return; + const poll = this.getPoll() orelse return; + + poll.enableKeepingProcessAlive(event_loop); + } + + pub fn disableKeepingProcessAlive(this: *PosixWriter, event_loop: JSC.EventLoopHandle) void { + const poll = this.getPoll() orelse return; + poll.disableKeepingProcessAlive(event_loop); + } + + pub fn updateRef(this: *PosixWriter, event_loop: JSC.EventLoopHandle, value: bool) void { + if (value) { + this.enableKeepingProcessAlive(event_loop); + } else { + this.disableKeepingProcessAlive(event_loop); + } + } + + pub fn end(this: *PosixWriter) void { + if (this.is_done) { + return; + } + + this.is_done = true; + this.close(); + } + + pub fn close(this: *PosixWriter) void { + if (this.closed_without_reporting) { + this.closed_without_reporting = false; + std.debug.assert(this.getFd() == bun.invalid_fd); + onClose(@ptrCast(this.parent)); + return; + } + + this.handle.close(@ptrCast(this.parent), onClose); + } + + pub fn start(this: *PosixWriter, fd: bun.FileDescriptor, is_pollable: bool) JSC.Maybe(void) { + if (!is_pollable) { + this.close(); + this.handle = .{ .fd = fd }; + return JSC.Maybe(void){ .result = {} }; + } + + const loop = @as(*Parent, @ptrCast(this.parent)).eventLoop(); + var poll = this.getPoll() orelse brk: { + this.handle = .{ .poll = Async.FilePoll.init(loop, fd, .{}, PosixWriter, this) }; + break :brk this.handle.poll; + }; + + switch (poll.registerWithFd(loop.loop(), .writable, .dispatch, fd)) { + .err => |err| { + return JSC.Maybe(void){ .err = err }; + }, + .result => {}, + } + + return JSC.Maybe(void){ .result = {} }; + } + }; +} + +/// Will provide base behavior for pipe writers +/// The WindowsPipeWriter type should implement the following interface: +/// struct { +/// source: ?Source = null, +/// parent: *Parent = undefined, +/// is_done: bool = false, +/// pub fn startWithCurrentPipe(this: *WindowsPipeWriter) bun.JSC.Maybe(void), +/// fn onClosePipe(pipe: *uv.Pipe) callconv(.C) void, +/// }; +fn BaseWindowsPipeWriter( + comptime WindowsPipeWriter: type, + comptime Parent: type, +) type { + return struct { + pub fn getFd(this: *const WindowsPipeWriter) bun.FileDescriptor { + const pipe = this.source orelse return bun.invalid_fd; + return pipe.getFd(); + } + + pub fn hasRef(this: *const WindowsPipeWriter) bool { + if (this.is_done) { + return false; + } + if (this.source) |pipe| return pipe.hasRef(); + return false; + } + + pub fn enableKeepingProcessAlive(this: *WindowsPipeWriter, event_loop: anytype) void { + this.updateRef(event_loop, true); + } + + pub fn disableKeepingProcessAlive(this: *WindowsPipeWriter, event_loop: anytype) void { + this.updateRef(event_loop, false); + } + + fn onFileClose(handle: *uv.fs_t) callconv(.C) void { + const file = bun.cast(*Source.File, handle.data); + file.fs.deinit(); + bun.default_allocator.destroy(file); + } + + fn onPipeClose(handle: *uv.Pipe) callconv(.C) void { + const this = bun.cast(*uv.Pipe, handle.data); + bun.default_allocator.destroy(this); + } + + fn onTTYClose(handle: *uv.uv_tty_t) callconv(.C) void { + const this = bun.cast(*uv.uv_tty_t, handle.data); + bun.default_allocator.destroy(this); + } + + pub fn close(this: *WindowsPipeWriter) void { + this.is_done = true; + if (this.source) |source| { + switch (source) { + .file => |file| { + file.fs.deinit(); + file.fs.data = file; + _ = uv.uv_fs_close(uv.Loop.get(), &source.file.fs, source.file.file, @ptrCast(&onFileClose)); + }, + .pipe => |pipe| { + pipe.data = pipe; + pipe.close(onPipeClose); + }, + .tty => |tty| { + tty.data = tty; + tty.close(onTTYClose); + }, + } + this.source = null; + this.onCloseSource(); + } + } + + pub fn updateRef(this: *WindowsPipeWriter, _: anytype, value: bool) void { + if (this.source) |pipe| { + if (value) { + pipe.ref(); + } else { + pipe.unref(); + } + } + } + + pub fn setParent(this: *WindowsPipeWriter, parent: *Parent) void { + this.parent = parent; + if (!this.is_done) { + if (this.source) |pipe| { + pipe.setData(this); + } + } + } + + pub fn watch(_: *WindowsPipeWriter) void { + // no-op + } + + pub fn startWithPipe(this: *WindowsPipeWriter, pipe: *uv.Pipe) bun.JSC.Maybe(void) { + std.debug.assert(this.source == null); + this.source = .{ .pipe = pipe }; + this.setParent(this.parent); + return this.startWithCurrentPipe(); + } + + pub fn start(this: *WindowsPipeWriter, fd: bun.FileDescriptor, _: bool) bun.JSC.Maybe(void) { + std.debug.assert(this.source == null); + const source = switch (Source.open(uv.Loop.get(), fd)) { + .result => |source| source, + .err => |err| return .{ .err = err }, + }; + source.setData(this); + this.source = source; + this.setParent(this.parent); + return this.startWithCurrentPipe(); + } + + pub fn setPipe(this: *WindowsPipeWriter, pipe: *uv.Pipe) void { + this.source = .{ .pipe = pipe }; + this.setParent(this.parent); + } + + pub fn getStream(this: *const WindowsPipeWriter) ?*uv.uv_stream_t { + const source = this.source orelse return null; + if (source == .file) return null; + return source.toStream(); + } + }; +} + +pub fn WindowsBufferedWriter( + comptime Parent: type, + comptime onWrite: *const fn (*Parent, amount: usize, status: WriteStatus) void, + comptime onError: *const fn (*Parent, bun.sys.Error) void, + comptime onClose: ?*const fn (*Parent) void, + comptime getBuffer: *const fn (*Parent) []const u8, + comptime onWritable: ?*const fn (*Parent) void, +) type { + return struct { + source: ?Source = null, + parent: *Parent = undefined, + is_done: bool = false, + // we use only one write_req, any queued data in outgoing will be flushed after this ends + write_req: uv.uv_write_t = std.mem.zeroes(uv.uv_write_t), + write_buffer: uv.uv_buf_t = uv.uv_buf_t.init(""), + pending_payload_size: usize = 0, + + const WindowsWriter = @This(); + + pub usingnamespace BaseWindowsPipeWriter(WindowsWriter, Parent); + + fn onCloseSource(this: *WindowsWriter) void { + if (onClose) |onCloseFn| { + onCloseFn(this.parent); + } + } + + pub fn startWithCurrentPipe(this: *WindowsWriter) bun.JSC.Maybe(void) { + std.debug.assert(this.source != null); + this.is_done = false; + this.write(); + return .{ .result = {} }; + } + + fn onWriteComplete(this: *WindowsWriter, status: uv.ReturnCode) void { + const written = this.pending_payload_size; + this.pending_payload_size = 0; + if (status.toError(.write)) |err| { + this.close(); + onError(this.parent, err); + return; + } + const pending = this.getBufferInternal(); + const has_pending_data = (pending.len - written) == 0; + onWrite(this.parent, @intCast(written), if (this.is_done and !has_pending_data) .drained else .pending); + // is_done can be changed inside onWrite + if (this.is_done and !has_pending_data) { + // already done and end was called + this.close(); + return; + } + + if (onWritable) |onWritableFn| { + onWritableFn(this.parent); + } + } + + fn onFsWriteComplete(fs: *uv.fs_t) callconv(.C) void { + const this = bun.cast(*WindowsWriter, fs.data); + if (fs.result.toError(.write)) |err| { + this.close(); + onError(this.parent, err); + return; + } + this.onWriteComplete(.zero); + } + + pub fn write(this: *WindowsWriter) void { + const buffer = this.getBufferInternal(); + // if we are already done or if we have some pending payload we just wait until next write + if (this.is_done or this.pending_payload_size > 0 or buffer.len == 0) { + return; + } + + const pipe = this.source orelse return; + switch (pipe) { + .file => |file| { + this.pending_payload_size = buffer.len; + file.fs.deinit(); + file.fs.setData(this); + this.write_buffer = uv.uv_buf_t.init(buffer); + + if (uv.uv_fs_write(uv.Loop.get(), &file.fs, file.file, @ptrCast(&this.write_buffer), 1, -1, onFsWriteComplete).toError(.write)) |err| { + this.close(); + onError(this.parent, err); + } + }, + else => { + // the buffered version should always have a stable ptr + this.pending_payload_size = buffer.len; + this.write_buffer = uv.uv_buf_t.init(buffer); + if (this.write_req.write(pipe.toStream(), &this.write_buffer, this, onWriteComplete).asErr()) |write_err| { + this.close(); + onError(this.parent, write_err); + } + }, + } + } + + fn getBufferInternal(this: *WindowsWriter) []const u8 { + return getBuffer(this.parent); + } + + pub fn end(this: *WindowsWriter) void { + if (this.is_done) { + return; + } + + this.is_done = true; + if (this.pending_payload_size == 0) { + // will auto close when pending stuff get written + this.close(); + } + } + }; +} + +/// Basic std.ArrayList(u8) + u32 cursor wrapper +pub const StreamBuffer = struct { + list: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), + // should cursor be usize? + cursor: u32 = 0, + + pub fn reset(this: *StreamBuffer) void { + this.cursor = 0; + if (this.list.capacity > 32 * 1024) { + this.list.shrinkAndFree(std.mem.page_size); + } + this.list.clearRetainingCapacity(); + } + + pub fn size(this: *const StreamBuffer) usize { + return this.list.items.len - this.cursor; + } + + pub fn isEmpty(this: *const StreamBuffer) bool { + return this.size() == 0; + } + + pub fn isNotEmpty(this: *const StreamBuffer) bool { + return this.size() > 0; + } + + pub fn write(this: *StreamBuffer, buffer: []const u8) !void { + _ = try this.list.appendSlice(buffer); + } + + pub fn writeAssumeCapacity(this: *StreamBuffer, buffer: []const u8) void { + var byte_list = bun.ByteList.fromList(this.list); + defer this.list = byte_list.listManaged(this.list.allocator); + byte_list.appendSliceAssumeCapacity(buffer); + } + + pub fn ensureUnusedCapacity(this: *StreamBuffer, capacity: usize) !void { + var byte_list = bun.ByteList.fromList(this.list); + defer this.list = byte_list.listManaged(this.list.allocator); + + _ = try byte_list.ensureUnusedCapacity(this.list.allocator, capacity); + } + + pub fn writeTypeAsBytes(this: *StreamBuffer, comptime T: type, data: *const T) !void { + _ = try this.write(std.mem.asBytes(data)); + } + + pub fn writeTypeAsBytesAssumeCapacity(this: *StreamBuffer, comptime T: type, data: T) void { + var byte_list = bun.ByteList.fromList(this.list); + defer this.list = byte_list.listManaged(this.list.allocator); + byte_list.writeTypeAsBytesAssumeCapacity(T, data); + } + + pub fn writeLatin1(this: *StreamBuffer, buffer: []const u8) !void { + if (bun.strings.isAllASCII(buffer)) { + return this.write(buffer); + } + + var byte_list = bun.ByteList.fromList(this.list); + defer this.list = byte_list.listManaged(this.list.allocator); + + _ = try byte_list.writeLatin1(this.list.allocator, buffer); + } + + pub fn writeUTF16(this: *StreamBuffer, buffer: []const u16) !void { + var byte_list = bun.ByteList.fromList(this.list); + defer this.list = byte_list.listManaged(this.list.allocator); + + _ = try byte_list.writeUTF16(this.list.allocator, buffer); + } + + pub fn slice(this: *StreamBuffer) []const u8 { + return this.list.items[this.cursor..]; + } + + pub fn deinit(this: *StreamBuffer) void { + this.cursor = 0; + if (this.list.capacity > 0) { + this.list.clearAndFree(); + } + } +}; + +pub fn WindowsStreamingWriter( + comptime Parent: type, + /// reports the amount written and done means that we dont have any other pending data to send (but we may send more data) + comptime onWrite: fn (*Parent, amount: usize, status: WriteStatus) void, + comptime onError: fn (*Parent, bun.sys.Error) void, + comptime onWritable: ?fn (*Parent) void, + comptime onClose: fn (*Parent) void, +) type { + return struct { + source: ?Source = null, + parent: *Parent = undefined, + is_done: bool = false, + // we use only one write_req, any queued data in outgoing will be flushed after this ends + write_req: uv.uv_write_t = std.mem.zeroes(uv.uv_write_t), + write_buffer: uv.uv_buf_t = uv.uv_buf_t.init(""), + + // queue any data that we want to write here + outgoing: StreamBuffer = .{}, + // libuv requires a stable ptr when doing async so we swap buffers + current_payload: StreamBuffer = .{}, + // we preserve the last write result for simplicity + last_write_result: WriteResult = .{ .wrote = 0 }, + // some error happed? we will not report onClose only onError + closed_without_reporting: bool = false, + + pub usingnamespace BaseWindowsPipeWriter(WindowsWriter, Parent); + + fn onCloseSource(this: *WindowsWriter) void { + this.source = null; + if (!this.closed_without_reporting) { + onClose(this.parent); + } + } + + pub fn startWithCurrentPipe(this: *WindowsWriter) bun.JSC.Maybe(void) { + std.debug.assert(this.source != null); + this.is_done = false; + return .{ .result = {} }; + } + + pub fn hasPendingData(this: *const WindowsWriter) bool { + return (this.outgoing.isNotEmpty() or this.current_payload.isNotEmpty()); + } + + fn isDone(this: *WindowsWriter) bool { + // done is flags andd no more data queued? so we are done! + return this.is_done and !this.hasPendingData(); + } + + fn onWriteComplete(this: *WindowsWriter, status: uv.ReturnCode) void { + if (status.toError(.write)) |err| { + this.last_write_result = .{ .err = err }; + log("onWrite() = {s}", .{err.name()}); + + onError(this.parent, err); + this.closeWithoutReporting(); + return; + } + + // success means that we send all the data inside current_payload + const written = this.current_payload.size(); + this.current_payload.reset(); + + // if we dont have more outgoing data we report done in onWrite + const done = this.outgoing.isEmpty(); + const was_done = this.is_done; + + log("onWrite({d}) ({d} left)", .{ written, this.outgoing.size() }); + + if (was_done and done) { + // we already call .end lets close the connection + this.last_write_result = .{ .done = written }; + onWrite(this.parent, written, .end_of_file); + return; + } + // .end was not called yet + this.last_write_result = .{ .wrote = written }; + + // report data written + onWrite(this.parent, written, if (done) .drained else .pending); + + // process pending outgoing data if any + this.processSend(); + + // TODO: should we report writable? + if (onWritable) |onWritableFn| { + onWritableFn(this.parent); + } + } + + fn onFsWriteComplete(fs: *uv.fs_t) callconv(.C) void { + const this = bun.cast(*WindowsWriter, fs.data); + if (fs.result.toError(.write)) |err| { + this.close(); + onError(this.parent, err); + return; + } + + this.onWriteComplete(.zero); + } + + /// this tries to send more data returning if we are writable or not after this + fn processSend(this: *WindowsWriter) void { + log("processSend", .{}); + if (this.current_payload.isNotEmpty()) { + // we have some pending async request, the next outgoing data will be processed after this finish + this.last_write_result = .{ .pending = 0 }; + return; + } + + const bytes = this.outgoing.slice(); + // nothing todo (we assume we are writable until we try to write something) + if (bytes.len == 0) { + this.last_write_result = .{ .wrote = 0 }; + return; + } + + var pipe = this.source orelse { + const err = bun.sys.Error.fromCode(bun.C.E.PIPE, .pipe); + this.last_write_result = .{ .err = err }; + onError(this.parent, err); + this.closeWithoutReporting(); + return; + }; + + // current payload is empty we can just swap with outgoing + const temp = this.current_payload; + this.current_payload = this.outgoing; + this.outgoing = temp; + switch (pipe) { + .file => |file| { + file.fs.deinit(); + file.fs.setData(this); + this.write_buffer = uv.uv_buf_t.init(bytes); + + if (uv.uv_fs_write(uv.Loop.get(), &file.fs, file.file, @ptrCast(&this.write_buffer), 1, -1, onFsWriteComplete).toError(.write)) |err| { + this.last_write_result = .{ .err = err }; + onError(this.parent, err); + this.closeWithoutReporting(); + return; + } + }, + else => { + // enqueue the write + this.write_buffer = uv.uv_buf_t.init(bytes); + if (this.write_req.write(pipe.toStream(), &this.write_buffer, this, onWriteComplete).asErr()) |err| { + this.last_write_result = .{ .err = err }; + onError(this.parent, err); + this.closeWithoutReporting(); + return; + } + }, + } + this.last_write_result = .{ .pending = 0 }; + } + + const WindowsWriter = @This(); + + fn closeWithoutReporting(this: *WindowsWriter) void { + if (this.getFd() != bun.invalid_fd) { + std.debug.assert(!this.closed_without_reporting); + this.closed_without_reporting = true; + this.close(); + } + } + + pub fn deinit(this: *WindowsWriter) void { + // clean both buffers if needed + this.outgoing.deinit(); + this.current_payload.deinit(); + this.close(); + } + + fn writeInternal(this: *WindowsWriter, buffer: anytype, comptime writeFn: anytype) WriteResult { + if (this.is_done) { + return .{ .done = 0 }; + } + + const had_buffered_data = this.outgoing.isNotEmpty(); + writeFn(&this.outgoing, buffer) catch { + return .{ .err = bun.sys.Error.oom }; + }; + + if (had_buffered_data) { + return .{ .pending = 0 }; + } + this.processSend(); + return this.last_write_result; + } + + pub fn writeUTF16(this: *WindowsWriter, buf: []const u16) WriteResult { + return writeInternal(this, buf, StreamBuffer.writeUTF16); + } + + pub fn writeLatin1(this: *WindowsWriter, buffer: []const u8) WriteResult { + return writeInternal(this, buffer, StreamBuffer.writeLatin1); + } + + pub fn write(this: *WindowsWriter, buffer: []const u8) WriteResult { + return writeInternal(this, buffer, StreamBuffer.write); + } + + pub fn flush(this: *WindowsWriter) WriteResult { + if (this.is_done) { + return .{ .done = 0 }; + } + if (!this.hasPendingData()) { + return .{ .wrote = 0 }; + } + + this.processSend(); + return this.last_write_result; + } + + pub fn end(this: *WindowsWriter) void { + if (this.is_done) { + return; + } + + this.is_done = true; + this.closed_without_reporting = false; + // if we are done we can call close if not we wait all the data to be flushed + if (this.isDone()) { + this.close(); + } + } + }; +} + +pub const BufferedWriter = if (bun.Environment.isPosix) PosixBufferedWriter else WindowsBufferedWriter; +pub const StreamingWriter = if (bun.Environment.isPosix) PosixStreamingWriter else WindowsStreamingWriter; diff --git a/src/io/io.zig b/src/io/io.zig index d2d849450466b9..48bec26d2d91bd 100644 --- a/src/io/io.zig +++ b/src/io/io.zig @@ -13,6 +13,8 @@ const TimerHeap = heap.Intrusive(Timer, void, Timer.less); const os = std.os; const assert = std.debug.assert; +pub const Source = @import("./source.zig").Source; + pub const Loop = struct { pending: Request.Queue = .{}, waker: bun.Async.Waker, @@ -36,7 +38,7 @@ pub const Loop = struct { if (!@atomicRmw(bool, &has_loaded_loop, std.builtin.AtomicRmwOp.Xchg, true, .Monotonic)) { loop = Loop{ - .waker = bun.Async.Waker.init(bun.default_allocator) catch @panic("failed to initialize waker"), + .waker = bun.Async.Waker.init() catch @panic("failed to initialize waker"), }; if (comptime Environment.isLinux) { loop.epoll_fd = bun.toFD(std.os.epoll_create1(std.os.linux.EPOLL.CLOEXEC | 0) catch @panic("Failed to create epoll file descriptor")); @@ -926,4 +928,12 @@ pub const Poll = struct { pub const retry = bun.C.E.AGAIN; +pub const ReadState = @import("./pipes.zig").ReadState; pub const PipeReader = @import("./PipeReader.zig").PipeReader; +pub const BufferedReader = @import("./PipeReader.zig").BufferedReader; +pub const BufferedWriter = @import("./PipeWriter.zig").BufferedWriter; +pub const WriteResult = @import("./PipeWriter.zig").WriteResult; +pub const WriteStatus = @import("./PipeWriter.zig").WriteStatus; +pub const StreamingWriter = @import("./PipeWriter.zig").StreamingWriter; +pub const StreamBuffer = @import("./PipeWriter.zig").StreamBuffer; +pub const FileType = @import("./pipes.zig").FileType; diff --git a/src/io/io_darwin.zig b/src/io/io_darwin.zig index 917a4976e2e9bb..41dc4465c428b1 100644 --- a/src/io/io_darwin.zig +++ b/src/io/io_darwin.zig @@ -104,8 +104,8 @@ pub const Waker = struct { *anyopaque, ) bool; - pub fn init(allocator: std.mem.Allocator) !Waker { - return initWithFileDescriptor(allocator, try std.os.kqueue()); + pub fn init() !Waker { + return initWithFileDescriptor(bun.default_allocator, try std.os.kqueue()); } pub fn initWithFileDescriptor(allocator: std.mem.Allocator, kq: i32) !Waker { diff --git a/src/io/io_linux.zig b/src/io/io_linux.zig index 702fcc69db1efb..795b88c2bf98f9 100644 --- a/src/io/io_linux.zig +++ b/src/io/io_linux.zig @@ -146,18 +146,16 @@ const bun = @import("root").bun; pub const Waker = struct { fd: bun.FileDescriptor, - pub fn init(allocator: std.mem.Allocator) !Waker { - return initWithFileDescriptor(allocator, bun.toFD(try std.os.eventfd(0, 0))); + pub fn init() !Waker { + return initWithFileDescriptor(bun.toFD(try std.os.eventfd(0, 0))); } pub fn getFd(this: *const Waker) bun.FileDescriptor { return this.fd; } - pub fn initWithFileDescriptor(_: std.mem.Allocator, fd: bun.FileDescriptor) Waker { - return Waker{ - .fd = fd, - }; + pub fn initWithFileDescriptor(fd: bun.FileDescriptor) Waker { + return Waker{ .fd = fd }; } pub fn wait(this: Waker) void { diff --git a/src/io/pipes.zig b/src/io/pipes.zig new file mode 100644 index 00000000000000..71f0191896cff9 --- /dev/null +++ b/src/io/pipes.zig @@ -0,0 +1,105 @@ +const Async = @import("root").bun.Async; +const bun = @import("root").bun; +const Environment = bun.Environment; + +pub const PollOrFd = union(enum) { + /// When it's a pipe/fifo + poll: *Async.FilePoll, + + fd: bun.FileDescriptor, + closed: void, + + pub fn setOwner(this: *const PollOrFd, owner: anytype) void { + if (this.* == .poll) { + this.poll.owner.set(owner); + } + } + + pub fn getFd(this: *const PollOrFd) bun.FileDescriptor { + return switch (this.*) { + .closed => bun.invalid_fd, + .fd => this.fd, + .poll => this.poll.fd, + }; + } + + pub fn getPoll(this: *const PollOrFd) ?*Async.FilePoll { + return switch (this.*) { + .closed => null, + .fd => null, + .poll => this.poll, + }; + } + + pub fn closeImpl(this: *PollOrFd, ctx: ?*anyopaque, comptime onCloseFn: anytype, close_fd: bool) void { + const fd = this.getFd(); + var close_async = true; + if (this.* == .poll) { + // workaround kqueue bug. + // 1) non-blocking FIFO + // 2) open for writing only = fd 2, nonblock + // 3) open for reading only = fd 3, nonblock + // 4) write(3, "something") = 9 + // 5) read(2, buf, 9) = 9 + // 6) read(2, buf, 9) = -1 (EAGAIN) + // 7) ON ANOTHER THREAD: close(3) = 0, + // 8) kevent(2, EVFILT_READ, EV_ADD | EV_ENABLE | EV_DISPATCH, 0, 0, 0) = 0 + // 9) ??? No more events for fd 2 + if (comptime Environment.isMac) { + if (this.poll.flags.contains(.poll_writable) and this.poll.flags.contains(.nonblocking)) { + close_async = false; + } + } + this.poll.deinitForceUnregister(); + this.* = .{ .closed = {} }; + } + + if (fd != bun.invalid_fd) { + this.* = .{ .closed = {} }; + + //TODO: We should make this call compatible using bun.FileDescriptor + if (Environment.isWindows) { + bun.Async.Closer.close(bun.uvfdcast(fd), bun.windows.libuv.Loop.get()); + } else if (close_async and close_fd) { + bun.Async.Closer.close(fd, {}); + } else { + if (close_fd) _ = bun.sys.close(fd); + } + if (comptime @TypeOf(onCloseFn) != void) + onCloseFn(@alignCast(@ptrCast(ctx.?))); + } else { + this.* = .{ .closed = {} }; + } + } + + pub fn close(this: *PollOrFd, ctx: ?*anyopaque, comptime onCloseFn: anytype) void { + this.closeImpl(ctx, onCloseFn, true); + } +}; + +pub const FileType = enum { + file, + pipe, + nonblocking_pipe, + socket, + + pub fn isPollable(this: FileType) bool { + return this == .pipe or this == .nonblocking_pipe or this == .socket; + } + + pub fn isBlocking(this: FileType) bool { + return this == .pipe; + } +}; + +pub const ReadState = enum { + /// The most common scenario + /// Neither EOF nor EAGAIN + progress, + + /// Received a 0-byte read + eof, + + /// Received an EAGAIN + drained, +}; diff --git a/src/io/source.zig b/src/io/source.zig new file mode 100644 index 00000000000000..44e9d2d9657491 --- /dev/null +++ b/src/io/source.zig @@ -0,0 +1,164 @@ +const std = @import("std"); +const bun = @import("root").bun; +const uv = bun.windows.libuv; + +const log = bun.Output.scoped(.PipeSource, false); + +pub const Source = union(enum) { + pipe: *Pipe, + tty: *Tty, + file: *File, + + const Pipe = uv.Pipe; + const Tty = uv.uv_tty_t; + pub const File = struct { + fs: uv.fs_t, + iov: uv.uv_buf_t, + file: uv.uv_file, + }; + + pub fn isClosed(this: Source) bool { + switch (this) { + .pipe => |pipe| return pipe.isClosed(), + .tty => |tty| return tty.isClosed(), + .file => |file| return file.file == -1, + } + } + + pub fn isActive(this: Source) bool { + switch (this) { + .pipe => |pipe| return pipe.isActive(), + .tty => |tty| return tty.isActive(), + .file => return true, + } + } + + pub fn getHandle(this: Source) *uv.Handle { + switch (this) { + .pipe => return @ptrCast(this.pipe), + .tty => return @ptrCast(this.tty), + .file => unreachable, + } + } + pub fn toStream(this: Source) *uv.uv_stream_t { + switch (this) { + .pipe => return @ptrCast(this.pipe), + .tty => return @ptrCast(this.tty), + .file => unreachable, + } + } + + pub fn getFd(this: Source) bun.FileDescriptor { + switch (this) { + .pipe => return this.pipe.fd(), + .tty => return this.tty.fd(), + .file => return bun.FDImpl.fromUV(this.file.file).encode(), + } + } + + pub fn setData(this: Source, data: ?*anyopaque) void { + switch (this) { + .pipe => this.pipe.data = data, + .tty => this.tty.data = data, + .file => this.file.fs.data = data, + } + } + + pub fn getData(this: Source) ?*anyopaque { + switch (this) { + .pipe => |pipe| return pipe.data, + .tty => |tty| return tty.data, + .file => |file| return file.fs.data, + } + } + + pub fn ref(this: Source) void { + switch (this) { + .pipe => this.pipe.ref(), + .tty => this.tty.ref(), + .file => return, + } + } + + pub fn unref(this: Source) void { + switch (this) { + .pipe => this.pipe.unref(), + .tty => this.tty.unref(), + .file => return, + } + } + + pub fn hasRef(this: Source) bool { + switch (this) { + .pipe => return this.pipe.hasRef(), + .tty => return this.tty.hasRef(), + .file => return false, + } + } + + pub fn openPipe(loop: *uv.Loop, fd: bun.FileDescriptor) bun.JSC.Maybe(*Source.Pipe) { + log("openPipe (fd = {})", .{fd}); + const pipe = bun.default_allocator.create(Source.Pipe) catch bun.outOfMemory(); + // we should never init using IPC here see ipc.zig + switch (pipe.init(loop, false)) { + .err => |err| { + return .{ .err = err }; + }, + else => {}, + } + + const file_fd = bun.uvfdcast(fd); + + return switch (pipe.open(file_fd)) { + .err => |err| .{ + .err = err, + }, + .result => .{ + .result = pipe, + }, + }; + } + + pub fn openTty(loop: *uv.Loop, fd: bun.FileDescriptor) bun.JSC.Maybe(*Source.Tty) { + log("openTTY (fd = {})", .{fd}); + const tty = bun.default_allocator.create(Source.Tty) catch bun.outOfMemory(); + + return switch (tty.init(loop, bun.uvfdcast(fd))) { + .err => |err| .{ .err = err }, + .result => .{ .result = tty }, + }; + } + + pub fn openFile(fd: bun.FileDescriptor) *Source.File { + log("openFile (fd = {})", .{fd}); + const file = bun.default_allocator.create(Source.File) catch bun.outOfMemory(); + + file.* = std.mem.zeroes(Source.File); + file.file = bun.uvfdcast(fd); + return file; + } + + pub fn open(loop: *uv.Loop, fd: bun.FileDescriptor) bun.JSC.Maybe(Source) { + log("open (fd = {})", .{fd}); + const rc = bun.windows.GetFileType(fd.cast()); + switch (rc) { + bun.windows.FILE_TYPE_PIPE => { + switch (openPipe(loop, fd)) { + .result => |pipe| return .{ .result = .{ .pipe = pipe } }, + .err => |err| return .{ .err = err }, + } + }, + bun.windows.FILE_TYPE_CHAR => { + switch (openTty(loop, fd)) { + .result => |tty| return .{ .result = .{ .tty = tty } }, + .err => |err| return .{ .err = err }, + } + }, + else => { + return .{ .result = .{ + .file = openFile(fd), + } }; + }, + } + } +}; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 3604f8df6deefc..68c84808b98fb9 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -480,8 +480,7 @@ declare interface PromiseConstructor extends ClassWithIntrinsics { + return { ...internalEnv }; + }; + return new Proxy(internalEnv, { get(_, p) { return typeof p === "string" ? internalEnv[p.toUpperCase()] : undefined; diff --git a/src/js/builtins/ReadableByteStreamInternals.ts b/src/js/builtins/ReadableByteStreamInternals.ts index 381f7201b2a89a..1a87c977ee1568 100644 --- a/src/js/builtins/ReadableByteStreamInternals.ts +++ b/src/js/builtins/ReadableByteStreamInternals.ts @@ -139,7 +139,7 @@ export function readableByteStreamControllerClose(controller) { } } - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); } export function readableByteStreamControllerClearPendingPullIntos(controller) { @@ -177,7 +177,7 @@ export function readableByteStreamControllerHandleQueueDrain(controller) { $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, ); if (!$getByIdDirectPrivate(controller, "queue").size && $getByIdDirectPrivate(controller, "closeRequested")) - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); else $readableByteStreamControllerCallPullIfNeeded(controller); } @@ -224,18 +224,16 @@ export function readableByteStreamControllerPull(controller) { export function readableByteStreamControllerShouldCallPull(controller) { $assert(controller); const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - $assert(stream); + if (!stream) { + return false; + } if ($getByIdDirectPrivate(stream, "state") !== $streamReadable) return false; if ($getByIdDirectPrivate(controller, "closeRequested")) return false; if (!($getByIdDirectPrivate(controller, "started") > 0)) return false; const reader = $getByIdDirectPrivate(stream, "reader"); - if ( - reader && - ($getByIdDirectPrivate(reader, "readRequests")?.isNotEmpty() || !!$getByIdDirectPrivate(reader, "bunNativePtr")) - ) - return true; + if (reader && ($getByIdDirectPrivate(reader, "readRequests")?.isNotEmpty() || !!reader.$bunNativePtr)) return true; if ( $readableStreamHasBYOBReader(stream) && $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests")?.isNotEmpty() @@ -283,7 +281,7 @@ export function transferBufferToCurrentRealm(buffer) { } export function readableStreamReaderKind(reader) { - if (!!$getByIdDirectPrivate(reader, "readRequests")) return $getByIdDirectPrivate(reader, "bunNativePtr") ? 3 : 1; + if (!!$getByIdDirectPrivate(reader, "readRequests")) return reader.$bunNativePtr ? 3 : 1; if (!!$getByIdDirectPrivate(reader, "readIntoRequests")) return 2; @@ -388,7 +386,6 @@ export function readableByteStreamControllerRespondInternal(controller, bytesWri let stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { - if (bytesWritten !== 0) throw new TypeError("bytesWritten is different from 0 even though stream is closed"); $readableByteStreamControllerRespondInClosedState(controller, firstDescriptor); } else { $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index 079fc129f2ac37..0108c08154503f 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -29,8 +29,7 @@ export function initializeReadableStream( underlyingSource: UnderlyingSource, strategy: QueuingStrategy, ) { - if (underlyingSource === undefined) - underlyingSource = { $bunNativeType: 0, $bunNativePtr: 0, $lazy: false } as UnderlyingSource; + if (underlyingSource === undefined) underlyingSource = { $bunNativePtr: undefined, $lazy: false } as UnderlyingSource; if (strategy === undefined) strategy = {}; if (!$isObject(underlyingSource)) throw new TypeError("ReadableStream constructor takes an object as first argument"); @@ -44,12 +43,11 @@ export function initializeReadableStream( $putByIdDirectPrivate(this, "storedError", undefined); - $putByIdDirectPrivate(this, "disturbed", false); + this.$disturbed = false; // Initialized with null value to enable distinction with undefined case. $putByIdDirectPrivate(this, "readableStreamController", null); - $putByIdDirectPrivate(this, "bunNativeType", $getByIdDirectPrivate(underlyingSource, "bunNativeType") ?? 0); - $putByIdDirectPrivate(this, "bunNativePtr", $getByIdDirectPrivate(underlyingSource, "bunNativePtr") ?? 0); + this.$bunNativePtr = $getByIdDirectPrivate(underlyingSource, "bunNativePtr") ?? undefined; $putByIdDirectPrivate(this, "asyncContext", $getInternalField($asyncContext, 0)); @@ -300,10 +298,10 @@ export function createUsedReadableStream() { } $linkTimeConstant; -export function createNativeReadableStream(nativePtr, nativeType, autoAllocateChunkSize) { +export function createNativeReadableStream(nativePtr, autoAllocateChunkSize) { + $assert(nativePtr, "nativePtr must be a valid pointer"); return new ReadableStream({ $lazy: true, - $bunNativeType: nativeType, $bunNativePtr: nativePtr, autoAllocateChunkSize: autoAllocateChunkSize, }); diff --git a/src/js/builtins/ReadableStreamDefaultReader.ts b/src/js/builtins/ReadableStreamDefaultReader.ts index 169806c52659d1..2ff8e385f04c27 100644 --- a/src/js/builtins/ReadableStreamDefaultReader.ts +++ b/src/js/builtins/ReadableStreamDefaultReader.ts @@ -51,7 +51,7 @@ export function readMany(this: ReadableStreamDefaultReader): ReadableStreamDefau if (!stream) throw new TypeError("readMany() called on a reader owned by no readable stream"); const state = $getByIdDirectPrivate(stream, "state"); - $putByIdDirectPrivate(stream, "disturbed", true); + stream.$disturbed = true; if (state === $streamClosed) return { value: [], size: 0, done: true }; else if (state === $streamErrored) { throw $getByIdDirectPrivate(stream, "storedError"); @@ -99,12 +99,11 @@ export function readMany(this: ReadableStreamDefaultReader): ReadableStreamDefau $putByValDirect(outValues, i, values[i].value); } } - $resetQueue($getByIdDirectPrivate(controller, "queue")); - if ($getByIdDirectPrivate(controller, "closeRequested")) - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); - else if ($isReadableStreamDefaultController(controller)) { + if ($getByIdDirectPrivate(controller, "closeRequested")) { + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); + } else if ($isReadableStreamDefaultController(controller)) { $readableStreamDefaultControllerCallPullIfNeeded(controller); } else if ($isReadableByteStreamController(controller)) { $readableByteStreamControllerCallPullIfNeeded(controller); @@ -141,7 +140,7 @@ export function readMany(this: ReadableStreamDefaultReader): ReadableStreamDefau $resetQueue(queue); if ($getByIdDirectPrivate(controller, "closeRequested")) { - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); } else if ($isReadableStreamDefaultController(controller)) { $readableStreamDefaultControllerCallPullIfNeeded(controller); } else if ($isReadableByteStreamController(controller)) { diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts index 53ebaaba1c4ca1..5b81edd9b32215 100644 --- a/src/js/builtins/ReadableStreamInternals.ts +++ b/src/js/builtins/ReadableStreamInternals.ts @@ -243,7 +243,7 @@ export function readableStreamPipeToWritableStream( pipeState.reader = $acquireReadableStreamDefaultReader(source); pipeState.writer = $acquireWritableStreamDefaultWriter(destination); - $putByIdDirectPrivate(source, "disturbed", true); + source.$disturbed = true; pipeState.finalized = false; pipeState.shuttingDown = false; @@ -708,7 +708,7 @@ export async function readStreamIntoSink(stream, sink, isNative) { sink, stream, undefined, - () => !didThrow && $markPromiseAsHandled(stream.cancel()), + () => !didThrow && stream.$state !== $streamClosed && $markPromiseAsHandled(stream.cancel()), stream.$asyncContext, ); @@ -778,7 +778,7 @@ export async function readStreamIntoSink(stream, sink, isNative) { } if (!didThrow && streamState !== $streamClosed && streamState !== $streamErrored) { - $readableStreamClose(stream); + $readableStreamCloseIfPossible(stream); } stream = undefined; } @@ -944,7 +944,7 @@ export function onCloseDirectStream(reason) { if (_pendingRead && $isPromise(_pendingRead) && flushed?.byteLength) { this._pendingRead = undefined; $fulfillPromise(_pendingRead, { value: flushed, done: false }); - $readableStreamClose(stream); + $readableStreamCloseIfPossible(stream); return; } } @@ -953,7 +953,7 @@ export function onCloseDirectStream(reason) { var requests = $getByIdDirectPrivate(reader, "readRequests"); if (requests?.isNotEmpty()) { $readableStreamFulfillReadRequest(stream, flushed, false); - $readableStreamClose(stream); + $readableStreamCloseIfPossible(stream); return; } @@ -964,7 +964,7 @@ export function onCloseDirectStream(reason) { done: false, }); flushed = undefined; - $readableStreamClose(stream); + $readableStreamCloseIfPossible(stream); stream = undefined; return thisResult; }; @@ -975,7 +975,7 @@ export function onCloseDirectStream(reason) { $fulfillPromise(read, { value: undefined, done: true }); } - $readableStreamClose(stream); + $readableStreamCloseIfPossible(stream); } export function onFlushDirectStream() { @@ -1325,7 +1325,7 @@ export function readableStreamDefaultControllerCallPullIfNeeded(controller) { export function isReadableStreamLocked(stream) { $assert($isReadableStream(stream)); - return !!$getByIdDirectPrivate(stream, "reader") || $getByIdDirectPrivate(stream, "bunNativePtr") === -1; + return !!$getByIdDirectPrivate(stream, "reader") || stream.$bunNativePtr === -1; } export function readableStreamDefaultControllerGetDesiredSize(controller) { @@ -1345,7 +1345,7 @@ export function readableStreamReaderGenericCancel(reader, reason) { } export function readableStreamCancel(stream, reason) { - $putByIdDirectPrivate(stream, "disturbed", true); + stream.$disturbed = true; const state = $getByIdDirectPrivate(stream, "state"); if (state === $streamClosed) return Promise.$resolve(); if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); @@ -1374,9 +1374,9 @@ export function readableStreamDefaultControllerPull(controller) { var queue = $getByIdDirectPrivate(controller, "queue"); if (queue.content.isNotEmpty()) { const chunk = $dequeueValue(queue); - if ($getByIdDirectPrivate(controller, "closeRequested") && queue.content.isEmpty()) - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); - else $readableStreamDefaultControllerCallPullIfNeeded(controller); + if ($getByIdDirectPrivate(controller, "closeRequested") && queue.content.isEmpty()) { + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); + } else $readableStreamDefaultControllerCallPullIfNeeded(controller); return $createFulfilledPromise({ value: chunk, done: false }); } @@ -1388,8 +1388,19 @@ export function readableStreamDefaultControllerPull(controller) { export function readableStreamDefaultControllerClose(controller) { $assert($readableStreamDefaultControllerCanCloseOrEnqueue(controller)); $putByIdDirectPrivate(controller, "closeRequested", true); - if ($getByIdDirectPrivate(controller, "queue")?.content?.isEmpty()) - $readableStreamClose($getByIdDirectPrivate(controller, "controlledReadableStream")); + if ($getByIdDirectPrivate(controller, "queue")?.content?.isEmpty()) { + $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); + } +} + +export function readableStreamCloseIfPossible(stream) { + switch ($getByIdDirectPrivate(stream, "state")) { + case $streamReadable: + case $streamClosing: { + $readableStreamClose(stream); + break; + } + } } export function readableStreamClose(stream) { @@ -1398,12 +1409,13 @@ export function readableStreamClose(stream) { $getByIdDirectPrivate(stream, "state") === $streamClosing, ); $putByIdDirectPrivate(stream, "state", $streamClosed); - if (!$getByIdDirectPrivate(stream, "reader")) return; + const reader = $getByIdDirectPrivate(stream, "reader"); + if (!reader) return; - if ($isReadableStreamDefaultReader($getByIdDirectPrivate(stream, "reader"))) { - const requests = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests"); + if ($isReadableStreamDefaultReader(reader)) { + const requests = $getByIdDirectPrivate(reader, "readRequests"); if (requests.isNotEmpty()) { - $putByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests", $createFIFO()); + $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); for (var request = requests.shift(); request; request = requests.shift()) $fulfillPromise(request, { value: undefined, done: true }); @@ -1449,7 +1461,7 @@ export function readableStreamDefaultReaderRead(reader) { $assert(!!stream); const state = $getByIdDirectPrivate(stream, "state"); - $putByIdDirectPrivate(stream, "disturbed", true); + stream.$disturbed = true; if (state === $streamClosed) return $createFulfilledPromise({ value: undefined, done: true }); if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); $assert(state === $streamReadable); @@ -1472,7 +1484,7 @@ export function readableStreamAddReadRequest(stream) { export function isReadableStreamDisturbed(stream) { $assert($isReadableStream(stream)); - return $getByIdDirectPrivate(stream, "disturbed"); + return stream.$disturbed; } $visibility = "Private"; @@ -1494,7 +1506,7 @@ export function readableStreamReaderGenericRelease(reader) { $markPromiseAsHandled(promise); var stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); - if ($getByIdDirectPrivate(stream, "bunNativeType") != 0) { + if (stream.$bunNativePtr) { $getByIdDirectPrivate($getByIdDirectPrivate(stream, "readableStreamController"), "underlyingByteSource").$resume( false, ); @@ -1608,11 +1620,10 @@ export function readableStreamFromAsyncIterator(target, fn) { export function lazyLoadStream(stream, autoAllocateChunkSize) { $debug("lazyLoadStream", stream, autoAllocateChunkSize); - var nativeType = $getByIdDirectPrivate(stream, "bunNativeType"); - var nativePtr = $getByIdDirectPrivate(stream, "bunNativePtr"); - var Prototype = $lazyStreamPrototypeMap.$get(nativeType); + var handle = stream.$bunNativePtr; + if (handle === -1) return; + var Prototype = $lazyStreamPrototypeMap.$get($getPrototypeOf(handle)); if (Prototype === undefined) { - var [pull, start, cancel, setClose, deinit, setRefOrUnref, drain] = $lazy(nativeType); var closer = [false]; var handleResult; function handleNativeReadableStreamPromiseResult(val) { @@ -1624,18 +1635,26 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { function callClose(controller) { try { - if ( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === - $streamReadable - ) { - controller.close(); + var underlyingByteSource = controller.$underlyingByteSource; + const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); + if (!stream) { + return; } + + if ($getByIdDirectPrivate(stream, "state") !== $streamReadable) return; + controller.close(); } catch (e) { globalThis.reportError(e); + } finally { + if (underlyingByteSource?.$stream) { + underlyingByteSource.$stream = undefined; + } } } handleResult = function handleResult(result, controller, view) { + $assert(controller, "controller is missing"); + if (result && $isPromise(result)) { return result.then( handleNativeReadableStreamPromiseResult.bind({ @@ -1645,12 +1664,12 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { err => controller.error(err), ); } else if (typeof result === "number") { - if (view && view.byteLength === result && view.buffer === controller.byobRequest?.view?.buffer) { + if (view && view.byteLength === result && view.buffer === controller?.byobRequest?.view?.buffer) { controller.byobRequest.respondWithNewView(view); } else { controller.byobRequest.respond(result); } - } else if (result.constructor === $Uint8Array) { + } else if ($isTypedArrayView(result)) { controller.enqueue(result); } @@ -1660,12 +1679,12 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { } }; - function createResult(tag, controller, view, closer) { + function createResult(handle, controller, view, closer) { closer[0] = false; var result; try { - result = pull(tag, view, closer); + result = handle.pull(view, closer); } catch (err) { return controller.error(err); } @@ -1673,27 +1692,32 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { return handleResult(result, controller, view); } - const registry = deinit ? new FinalizationRegistry(deinit) : null; Prototype = class NativeReadableStreamSource { - constructor(tag, autoAllocateChunkSize, drainValue) { - $putByIdDirectPrivate(this, "stream", tag); - this.#cancellationToken = {}; + constructor(handle, autoAllocateChunkSize, drainValue) { + $putByIdDirectPrivate(this, "stream", handle); this.pull = this.#pull.bind(this); this.cancel = this.#cancel.bind(this); this.autoAllocateChunkSize = autoAllocateChunkSize; if (drainValue !== undefined) { this.start = controller => { + this.#controller = new WeakRef(controller); controller.enqueue(drainValue); }; } - if (registry) { - registry.register(this, tag, this.#cancellationToken); + handle.onClose = this.#onClose.bind(this); + handle.onDrain = this.#onDrain.bind(this); + } + + #onDrain(chunk) { + var controller = this.#controller?.deref?.(); + if (controller) { + controller.enqueue(chunk); } } - #cancellationToken; + #controller: WeakRef; pull; cancel; @@ -1701,58 +1725,76 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { type = "bytes"; autoAllocateChunkSize = 0; + #closed = false; + + #onClose() { + this.#closed = true; + this.#controller = undefined; + + var controller = this.#controller?.deref?.(); - static startSync = start; + $putByIdDirectPrivate(this, "stream", undefined); + if (controller) { + $enqueueJob(callClose, controller); + } + } #pull(controller) { - var tag = $getByIdDirectPrivate(this, "stream"); + var handle = $getByIdDirectPrivate(this, "stream"); - if (!tag) { - controller.close(); + if (!handle || this.#closed) { + this.#controller = undefined; + $putByIdDirectPrivate(this, "stream", undefined); + $enqueueJob(callClose, controller); return; } - createResult(tag, controller, controller.byobRequest.view, closer); + if (!this.#controller) { + this.#controller = new WeakRef(controller); + } + + createResult(handle, controller, controller.byobRequest.view, closer); } #cancel(reason) { - var tag = $getByIdDirectPrivate(this, "stream"); - - registry && registry.unregister(this.#cancellationToken); - setRefOrUnref && setRefOrUnref(tag, false); - cancel(tag, reason); + var handle = $getByIdDirectPrivate(this, "stream"); + if (handle) { + handle.updateRef(false); + handle.cancel(reason); + $putByIdDirectPrivate(this, "stream", undefined); + } } - - static deinit = deinit; - static drain = drain; }; // this is reuse of an existing private symbol Prototype.prototype.$resume = function (has_ref) { - var tag = $getByIdDirectPrivate(this, "stream"); - setRefOrUnref && setRefOrUnref(tag, has_ref); + var handle = $getByIdDirectPrivate(this, "stream"); + if (handle) handle.updateRef(has_ref); }; - $lazyStreamPrototypeMap.$set(nativeType, Prototype); + $lazyStreamPrototypeMap.$set($getPrototypeOf(handle), Prototype); } - $putByIdDirectPrivate(stream, "disturbed", true); - - const chunkSize = Prototype.startSync(nativePtr, autoAllocateChunkSize); - var drainValue; - const { drain: drainFn, deinit: deinitFn } = Prototype; - if (drainFn) { - drainValue = drainFn(nativePtr); + stream.$disturbed = true; + const chunkSizeOrCompleteBuffer = handle.start(autoAllocateChunkSize); + let chunkSize, drainValue; + if ($isTypedArrayView(chunkSizeOrCompleteBuffer)) { + chunkSize = 0; + drainValue = chunkSizeOrCompleteBuffer; + } else { + chunkSize = chunkSizeOrCompleteBuffer; + drainValue = handle.drain(); } // empty file, no need for native back-and-forth on this if (chunkSize === 0) { - deinit && nativePtr && $enqueueJob(deinit, nativePtr); - if ((drainValue?.byteLength ?? 0) > 0) { return { start(controller) { controller.enqueue(drainValue); controller.close(); }, + pull(controller) { + controller.close(); + }, type: "bytes", }; } @@ -1761,11 +1803,14 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { start(controller) { controller.close(); }, + pull(controller) { + controller.close(); + }, type: "bytes", }; } - return new Prototype(nativePtr, chunkSize, drainValue); + return new Prototype(handle, chunkSize, drainValue); } export function readableStreamIntoArray(stream) { @@ -1862,7 +1907,7 @@ export function readableStreamToArrayBufferDirect(stream, underlyingSource) { return Promise.$reject(e); } finally { if (!$isPromise(firstPull)) { - if (!didError && stream) $readableStreamClose(stream); + if (!didError && stream) $readableStreamCloseIfPossible(stream); controller = close = sink = pull = stream = undefined; return capability.promise; } @@ -1871,7 +1916,7 @@ export function readableStreamToArrayBufferDirect(stream, underlyingSource) { $assert($isPromise(firstPull)); return firstPull.then( () => { - if (!didError && stream) $readableStreamClose(stream); + if (!didError && stream) $readableStreamCloseIfPossible(stream); controller = close = sink = pull = stream = undefined; return capability.promise; }, @@ -1955,7 +2000,7 @@ export function readableStreamDefineLazyIterators(prototype) { } finally { reader.releaseLock(); - if (!preventCancel) { + if (!preventCancel && !$isReadableStreamLocked(stream)) { stream.cancel(deferredError); } diff --git a/src/js/node/child_process.js b/src/js/node/child_process.js index fc35d0841f784c..f97ca558397383 100644 --- a/src/js/node/child_process.js +++ b/src/js/node/child_process.js @@ -153,17 +153,19 @@ function spawn(file, args, options) { $debug("spawn", options); child.spawn(options); - if (options.timeout > 0) { + const timeout = options.timeout; + if (timeout && timeout > 0) { let timeoutId = setTimeout(() => { if (timeoutId) { + timeoutId = null; + try { child.kill(killSignal); } catch (err) { child.emit("error", err); } - timeoutId = null; } - }, options.timeout); + }, timeout).unref(); child.once("exit", () => { if (timeoutId) { @@ -173,8 +175,8 @@ function spawn(file, args, options) { }); } - if (options.signal) { - const signal = options.signal; + const signal = options.signal; + if (signal) { if (signal.aborted) { process.nextTick(onAbortListener); } else { @@ -183,7 +185,7 @@ function spawn(file, args, options) { } function onAbortListener() { - abortChildProcess(child, killSignal, options.signal.reason); + abortChildProcess(child, killSignal, signal.reason); } } process.nextTick(() => { @@ -341,9 +343,9 @@ function execFile(file, args, options, callback) { if (options.timeout > 0) { timeoutId = setTimeout(function delayedKill() { - kill(); + timeoutId && kill(); timeoutId = null; - }, options.timeout); + }, options.timeout).unref(); } if (child.stdout) { @@ -629,10 +631,10 @@ function spawnSync(file, args, options) { function execFileSync(file, args, options) { ({ file, args, options } = normalizeExecFileArgs(file, args, options)); - // const inheritStderr = !options.stdio; + const inheritStderr = !options.stdio; const ret = spawnSync(file, args, options); - // if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); + if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); const errArgs = [options.argv0 || file]; ArrayPrototypePush.$apply(errArgs, args); @@ -664,11 +666,11 @@ function execFileSync(file, args, options) { */ function execSync(command, options) { const opts = normalizeExecArgs(command, options, null); - // const inheritStderr = !opts.options.stdio; + const inheritStderr = !opts.options.stdio; const ret = spawnSync(opts.file, opts.options); - // if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); // TODO: Uncomment when we have process.stderr + if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr); const err = checkExecSyncError(ret, undefined, command); @@ -927,7 +929,7 @@ function normalizeSpawnArguments(file, args, options) { else file = process.env.comspec || "cmd.exe"; // '/d /s /c' is used only for cmd.exe. if (/^(?:.*\\)?cmd(?:\.exe)?$/i.exec(file) !== null) { - args = ["/d", "/s", "/c", `"${command}"`]; + args = ["/d", "/s", "/c", command]; windowsVerbatimArguments = true; } else { args = ["-c", command]; @@ -935,7 +937,7 @@ function normalizeSpawnArguments(file, args, options) { } else { if (typeof options.shell === "string") file = options.shell; else if (process.platform === "android") file = "sh"; - else file = "sh"; + else file = "/bin/sh"; args = ["-c", command]; } } @@ -978,7 +980,6 @@ function checkExecSyncError(ret, args, cmd) { //------------------------------------------------------------------------------ class ChildProcess extends EventEmitter { #handle; - #exited = false; #closesNeeded = 1; #closesGot = 0; @@ -996,26 +997,54 @@ class ChildProcess extends EventEmitter { // constructor(options) { // super(options); - // this.#handle[owner_symbol] = this; // } #handleOnExit(exitCode, signalCode, err) { - if (this.#exited) return; if (signalCode) { this.signalCode = signalCode; } else { this.exitCode = exitCode; } - if (this.#stdin) { - this.#stdin.destroy(); + // Drain stdio streams + { + if (this.#stdin) { + this.#stdin.destroy(); + } else { + this.#stdioOptions[0] = "destroyed"; + } + + // If there was an error while spawning the subprocess, then we will never have any IO to drain. + if (err) { + this.#stdioOptions[1] = this.#stdioOptions[2] = "destroyed"; + } + + const stdout = this.#stdout, + stderr = this.#stderr; + + if (stdout === undefined) { + this.#stdout = this.#getBunSpawnIo(1, this.#encoding, true); + } else if (stdout && this.#stdioOptions[1] === "pipe" && !stdout?.destroyed) { + stdout.resume?.(); + } + + if (stderr === undefined) { + this.#stderr = this.#getBunSpawnIo(2, this.#encoding, true); + } else if (stderr && this.#stdioOptions[2] === "pipe" && !stderr?.destroyed) { + stderr.resume?.(); + } } if (this.#handle) { this.#handle = null; } - if (exitCode < 0) { + if (err) { + if (this.spawnfile) err.path = this.spawnfile; + err.spawnargs = ArrayPrototypeSlice.$call(this.spawnargs, 1); + err.pid = this.pid; + this.emit("error", err); + } else if (exitCode < 0) { const err = new SystemError( `Spawned process exited with error code: ${exitCode}`, undefined, @@ -1023,29 +1052,20 @@ class ChildProcess extends EventEmitter { "EUNKNOWN", "ERR_CHILD_PROCESS_UNKNOWN_ERROR", ); + err.pid = this.pid; if (this.spawnfile) err.path = this.spawnfile; err.spawnargs = ArrayPrototypeSlice.$call(this.spawnargs, 1); this.emit("error", err); - } else { - this.emit("exit", this.exitCode, this.signalCode); } - // If any of the stdio streams have not been touched, - // then pull all the data through so that it can get the - // eof and emit a 'close' event. - // Do it on nextTick so that the user has one last chance - // to consume the output, if for example they only want to - // start reading the data once the process exits. - process.nextTick(flushStdio, this); + this.emit("exit", this.exitCode, this.signalCode); this.#maybeClose(); - this.#exited = true; - this.#stdioOptions = ["destroyed", "destroyed", "destroyed"]; } - #getBunSpawnIo(i, encoding) { + #getBunSpawnIo(i, encoding, autoResume = false) { if ($debug && !this.#handle) { if (this.#handle === null) { $debug("ChildProcess: getBunSpawnIo: this.#handle is null. This means the subprocess already exited"); @@ -1056,7 +1076,6 @@ class ChildProcess extends EventEmitter { NativeWritable ||= StreamModule.NativeWritable; ReadableFromWeb ||= StreamModule.Readable.fromWeb; - if (!NetModule) NetModule = require("node:net"); const io = this.#stdioOptions[i]; switch (i) { @@ -1075,8 +1094,13 @@ class ChildProcess extends EventEmitter { case 2: case 1: { switch (io) { - case "pipe": - return ReadableFromWeb(this.#handle[fdToStdioName(i)], { encoding }); + case "pipe": { + const pipe = ReadableFromWeb(this.#handle[fdToStdioName(i)], { encoding }); + this.#closesNeeded++; + pipe.once("close", () => this.#maybeClose()); + if (autoResume) pipe.resume(); + return pipe; + } case "inherit": return process[fdToStdioName(i)] || null; case "destroyed": @@ -1088,6 +1112,7 @@ class ChildProcess extends EventEmitter { default: switch (io) { case "pipe": + if (!NetModule) NetModule = require("node:net"); const fd = this.#handle.stdio[i]; if (!fd) return null; return new NetModule.connect({ fd }); @@ -1104,9 +1129,12 @@ class ChildProcess extends EventEmitter { #stdioOptions; #createStdioObject() { - let result = new Array(this.#stdioOptions.length); - for (let i = 0; i < this.#stdioOptions.length; i++) { - const element = this.#stdioOptions[i]; + const opts = this.#stdioOptions; + const length = opts.length; + let result = new Array(length); + for (let i = 0; i < length; i++) { + const element = opts[i]; + if (element !== "pipe") { result[i] = null; continue; @@ -1122,7 +1150,7 @@ class ChildProcess extends EventEmitter { result[i] = this.stderr; continue; default: - result[i] = this.#getBunSpawnIo(i, this.#encoding); + result[i] = this.#getBunSpawnIo(i, this.#encoding, false); continue; } } @@ -1130,15 +1158,15 @@ class ChildProcess extends EventEmitter { } get stdin() { - return (this.#stdin ??= this.#getBunSpawnIo(0, this.#encoding)); + return (this.#stdin ??= this.#getBunSpawnIo(0, this.#encoding, false)); } get stdout() { - return (this.#stdout ??= this.#getBunSpawnIo(1, this.#encoding)); + return (this.#stdout ??= this.#getBunSpawnIo(1, this.#encoding, false)); } get stderr() { - return (this.#stderr ??= this.#getBunSpawnIo(2, this.#encoding)); + return (this.#stderr ??= this.#getBunSpawnIo(2, this.#encoding, false)); } get stdio() { @@ -1185,16 +1213,18 @@ class ChildProcess extends EventEmitter { const stdio = options.stdio || ["pipe", "pipe", "pipe"]; const bunStdio = getBunStdioFromOptions(stdio); + const argv0 = file || options.argv0; // TODO: better ipc support const ipc = $isArray(stdio) && stdio[3] === "ipc"; - var env = options.envPairs || undefined; const detachedOption = options.detached; this.#encoding = options.encoding || undefined; this.#stdioOptions = bunStdio; const stdioCount = stdio.length; const hasSocketsToEagerlyLoad = stdioCount >= 3; + this.#closesNeeded = 1; + this.#handle = Bun.spawn({ cmd: spawnargs, stdio: bunStdio, @@ -1218,6 +1248,7 @@ class ChildProcess extends EventEmitter { }, lazy: true, ipc: ipc ? this.#emitIpcMessage.bind(this) : undefined, + argv0, }); this.pid = this.#handle.pid; @@ -1231,7 +1262,9 @@ class ChildProcess extends EventEmitter { } if (hasSocketsToEagerlyLoad) { - this.stdio; + for (let item of this.stdio) { + item?.ref?.(); + } } } @@ -1293,8 +1326,6 @@ class ChildProcess extends EventEmitter { this.#handle.kill(signal); } - this.#maybeClose(); - // TODO: Figure out how to make this conform to the Node spec... // The problem is that the handle does not report killed until the process exits // So we can't return whether or not the process was killed because Bun.spawn seems to handle this async instead of sync like Node does @@ -1417,22 +1448,6 @@ function normalizeStdio(stdio) { } } -function flushStdio(subprocess) { - const stdio = subprocess.stdio; - if (stdio == null) return; - - for (let i = 0; i < stdio.length; i++) { - const stream = stdio[i]; - // TODO(addaleax): This doesn't necessarily account for all the ways in - // which data can be read from a stream, e.g. being consumed on the - // native layer directly as a StreamBase. - if (!stream || !stream.readable) { - continue; - } - stream.resume(); - } -} - function onSpawnNT(self) { self.emit("spawn"); } @@ -1456,12 +1471,30 @@ class ShimmedStdin extends EventEmitter { return false; } destroy() {} - end() {} - pipe() {} + end() { + return this; + } + pipe() { + return this; + } + resume() { + return this; + } } class ShimmedStdioOutStream extends EventEmitter { pipe() {} + get destroyed() { + return true; + } + + resume() { + return this; + } + + destroy() { + return this; + } } //------------------------------------------------------------------------------ @@ -1561,7 +1594,7 @@ const validateObject = (value, name, options = null) => { const nullable = options?.nullable ?? false; if ( (!nullable && value === null) || - (!allowArray && ArrayIsArray.$call(value)) || + (!allowArray && $isJSArray(value)) || (typeof value !== "object" && (!allowFunction || typeof value !== "function")) ) { throw new ERR_INVALID_ARG_TYPE(name, "object", value); diff --git a/src/js/node/events.js b/src/js/node/events.js index 12023474551811..532bd42a66baa8 100644 --- a/src/js/node/events.js +++ b/src/js/node/events.js @@ -397,6 +397,9 @@ function on(emitter, event, options = {}) { emitter.on(evName, () => { emitter.removeListener(event, eventHandler); emitter.removeListener("error", errorHandler); + while (!unconsumedPromises.isEmpty()) { + unconsumedPromises.shift().resolve(); + } done = true; }); } diff --git a/src/js/node/fs.js b/src/js/node/fs.js index c6dfc38be0b6c1..54bf1b4ff5ce0c 100644 --- a/src/js/node/fs.js +++ b/src/js/node/fs.js @@ -596,12 +596,11 @@ ReadStream = (function (InternalReadStream) { // Get the stream controller // We need the pointer to the underlying stream controller for the NativeReadable var stream = fileRef.stream(); - var native = $direct(stream); - if (!native) { + var ptr = stream.$bunNativePtr; + if (!ptr) { $debug("no native readable stream"); throw new Error("no native readable stream"); } - var { stream: ptr } = native; super(ptr, { ...options, diff --git a/src/js/node/stream.js b/src/js/node/stream.js index add04af424eea5..b194f2155d763b 100644 --- a/src/js/node/stream.js +++ b/src/js/node/stream.js @@ -3857,7 +3857,7 @@ var require_writable = __commonJS({ let called = false; function onFinish(err) { if (called) { - errorOrDestroy(stream, err !== null && err !== void 0 ? err : ERR_MULTIPLE_CALLBACK()); + errorOrDestroy(stream, err !== null && err !== void 0 ? err : new ERR_MULTIPLE_CALLBACK()); return; } called = true; @@ -5222,8 +5222,6 @@ var require_stream = __commonJS({ * */ function createNativeStreamReadable(nativeType, Readable) { - var [pull, start, cancel, setClose, deinit, updateRef, drainFn] = $lazy(nativeType); - var closer = [false]; var handleNumberResult = function (nativeReadable, result, view, isClosed) { if (result > 0) { @@ -5261,19 +5259,18 @@ function createNativeStreamReadable(nativeType, Readable) { var DYNAMICALLY_ADJUST_CHUNK_SIZE = process.env.BUN_DISABLE_DYNAMIC_CHUNK_SIZE !== "1"; - const finalizer = new FinalizationRegistry(ptr => ptr && deinit(ptr)); const MIN_BUFFER_SIZE = 512; var NativeReadable = class NativeReadable extends Readable { #bunNativePtr; - #refCount = 1; + #refCount = 0; #constructed = false; #remainingChunk = undefined; #highWaterMark; #pendingRead = false; #hasResized = !DYNAMICALLY_ADJUST_CHUNK_SIZE; - #unregisterToken; constructor(ptr, options = {}) { super(options); + if (typeof options.highWaterMark === "number") { this.#highWaterMark = options.highWaterMark; } else { @@ -5283,8 +5280,16 @@ function createNativeStreamReadable(nativeType, Readable) { this.#constructed = false; this.#remainingChunk = undefined; this.#pendingRead = false; - this.#unregisterToken = {}; - finalizer.register(this, this.#bunNativePtr, this.#unregisterToken); + ptr.onClose = this.#onClose.bind(this); + ptr.onDrain = this.#onDrain.bind(this); + } + + #onClose() { + this.push(null); + } + + #onDrain(chunk) { + this.push(chunk); } // maxToRead is by default the highWaterMark passed from the Readable.read call to this fn @@ -5299,7 +5304,7 @@ function createNativeStreamReadable(nativeType, Readable) { var ptr = this.#bunNativePtr; $debug("ptr @ NativeReadable._read", ptr, this.__id); - if (ptr === 0 || ptr === -1) { + if (!ptr) { this.push(null); return; } @@ -5331,7 +5336,9 @@ function createNativeStreamReadable(nativeType, Readable) { #internalConstruct(ptr) { this.#constructed = true; - const result = start(ptr, this.#highWaterMark); + + const result = ptr.start(this.#highWaterMark); + $debug("NativeReadable internal `start` result", result, this.__id); if (typeof result === "number" && result > 1) { @@ -5341,12 +5348,10 @@ function createNativeStreamReadable(nativeType, Readable) { this.#highWaterMark = Math.min(this.#highWaterMark, result); } - if (drainFn) { - const drainResult = drainFn(ptr); - $debug("NativeReadable drain result", drainResult, this.__id); - if ((drainResult?.byteLength ?? 0) > 0) { - this.push(drainResult); - } + const drainResult = ptr.drain(); + $debug("NativeReadable drain result", drainResult, this.__id); + if ((drainResult?.byteLength ?? 0) > 0) { + this.push(drainResult); } } @@ -5363,6 +5368,13 @@ function createNativeStreamReadable(nativeType, Readable) { return chunk; } + #adjustHighWaterMark() { + this.#highWaterMark = Math.min(this.#highWaterMark * 2, 1024 * 1024 * 2); + this.#hasResized = true; + + $debug("Resized", this.__id); + } + // push(result, encoding) { // debug("NativeReadable push -- result, encoding", result, encoding, this.__id); // return super.push(...arguments); @@ -5373,8 +5385,7 @@ function createNativeStreamReadable(nativeType, Readable) { if (typeof result === "number") { if (result >= this.#highWaterMark && !this.#hasResized && !isClosed) { - this.#highWaterMark *= 2; - this.#hasResized = true; + this.#adjustHighWaterMark(); } return handleNumberResult(this, result, view, isClosed); @@ -5383,11 +5394,9 @@ function createNativeStreamReadable(nativeType, Readable) { this.push(null); }); return view?.byteLength ?? 0 > 0 ? view : undefined; - } else if (ArrayBuffer.isView(result)) { + } else if ($isTypedArrayView(result)) { if (result.byteLength >= this.#highWaterMark && !this.#hasResized && !isClosed) { - this.#highWaterMark *= 2; - this.#hasResized = true; - $debug("Resized", this.__id); + this.#adjustHighWaterMark(); } return handleArrayBufferViewResult(this, result, view, isClosed); @@ -5400,14 +5409,15 @@ function createNativeStreamReadable(nativeType, Readable) { #internalRead(view, ptr) { $debug("#internalRead()", this.__id); closer[0] = false; - var result = pull(ptr, view, closer); + var result = ptr.pull(view, closer); if ($isPromise(result)) { this.#pendingRead = true; return result.then( result => { this.#pendingRead = false; $debug("pending no longerrrrrrrr (result returned from pull)", this.__id); - this.#remainingChunk = this.#handleResult(result, view, closer[0]); + const isClosed = closer[0]; + this.#remainingChunk = this.#handleResult(result, view, isClosed); }, reason => { $debug("error from pull", reason, this.__id); @@ -5421,43 +5431,36 @@ function createNativeStreamReadable(nativeType, Readable) { _destroy(error, callback) { var ptr = this.#bunNativePtr; - if (ptr === 0) { + if (!ptr) { callback(error); return; } - finalizer.unregister(this.#unregisterToken); - this.#bunNativePtr = 0; - if (updateRef) { - updateRef(ptr, false); - } + this.#bunNativePtr = undefined; + ptr.updateRef(false); + $debug("NativeReadable destroyed", this.__id); - cancel(ptr, error); + ptr.cancel(error); callback(error); } ref() { var ptr = this.#bunNativePtr; - if (ptr === 0) return; + if (ptr === undefined) return; if (this.#refCount++ === 0) { - updateRef(ptr, true); + ptr.updateRef(true); } } unref() { var ptr = this.#bunNativePtr; - if (ptr === 0) return; + if (ptr === undefined) return; if (this.#refCount-- === 1) { - updateRef(ptr, false); + ptr.updateRef(false); } } }; - if (!updateRef) { - NativeReadable.prototype.ref = undefined; - NativeReadable.prototype.unref = undefined; - } - return NativeReadable; } @@ -5474,19 +5477,19 @@ function getNativeReadableStreamPrototype(nativeType, Readable) { } function getNativeReadableStream(Readable, stream, options) { - if (!(stream && typeof stream === "object" && stream instanceof ReadableStream)) { - return undefined; - } - - const native = $direct(stream); - if (!native) { + const ptr = stream.$bunNativePtr; + if (!ptr || ptr === -1) { $debug("no native readable stream"); return undefined; } - const { stream: ptr, data: type } = native; + const type = stream.$bunNativeType; + $assert(typeof type === "number", "Invalid native type"); + $assert(typeof ptr === "object", "Invalid native ptr"); const NativeReadable = getNativeReadableStreamPrototype(type, Readable); - + stream.$bunNativePtr = -1; + stream.$bunNativeType = 0; + stream.$disturbed = true; return new NativeReadable(ptr, options); } /** --- Bun native stream wrapper --- */ @@ -5507,9 +5510,11 @@ function NativeWritable(pathOrFdOrSink, options = {}) { this._construct = NativeWritable_internalConstruct; this._destroy = NativeWritable_internalDestroy; this._final = NativeWritable_internalFinal; + this._write = NativeWritablePrototypeWrite; this[_pathOrFdOrSink] = pathOrFdOrSink; } +Object.setPrototypeOf(NativeWritable, Writable); NativeWritable.prototype = Object.create(Writable.prototype); // These are confusingly two different fns for construct which initially were the same thing because @@ -5518,7 +5523,7 @@ NativeWritable.prototype = Object.create(Writable.prototype); function NativeWritable_internalConstruct(cb) { this._writableState.constructed = true; this.constructed = true; - if (typeof cb === "function") cb(); + if (typeof cb === "function") process.nextTick(cb); process.nextTick(() => { this.emit("open", this.fd); this.emit("ready"); @@ -5540,36 +5545,41 @@ function NativeWritable_lazyConstruct(stream) { } const WritablePrototypeWrite = Writable.prototype.write; -NativeWritable.prototype.write = function NativeWritablePrototypeWrite(chunk, encoding, cb, native) { - if (!(native ?? this[_native])) { - this[_native] = false; - return WritablePrototypeWrite.$call(this, chunk, encoding, cb); - } - +function NativeWritablePrototypeWrite(chunk, encoding, cb) { var fileSink = this[_fileSink] ?? NativeWritable_lazyConstruct(this); var result = fileSink.write(chunk); + if (typeof encoding === "function") { + cb = encoding; + } + if ($isPromise(result)) { // var writePromises = this.#writePromises; // var i = writePromises.length; // writePromises[i] = result; - result.then(() => { - this.emit("drain"); - fileSink.flush(true); - // // We can't naively use i here because we don't know when writes will resolve necessarily - // writePromises.splice(writePromises.indexOf(result), 1); - }); + result + .then(result => { + this.emit("drain"); + if (cb) { + cb(null, result); + } + }) + .catch( + cb + ? err => { + cb(err); + } + : err => { + this.emit("error", err); + }, + ); return false; } - fileSink.flush(true); - if (typeof encoding === "function") { - cb = encoding; - } // TODO: Should we just have a calculation based on encoding and length of chunk? if (cb) cb(null, chunk.byteLength); return true; -}; +} const WritablePrototypeEnd = Writable.prototype.end; NativeWritable.prototype.end = function end(chunk, encoding, cb, native) { return WritablePrototypeEnd.$call(this, chunk, encoding, cb, native ?? this[_native]); @@ -5598,21 +5608,26 @@ function NativeWritable_internalDestroy(error, cb) { function NativeWritable_internalFinal(cb) { var sink = this[_fileSink]; if (sink) { - sink.end(); + const end = sink.end(true); + if ($isPromise(end) && cb) { + end.then(() => { + if (cb) cb(); + }, cb); + } } if (cb) cb(); } NativeWritable.prototype.ref = function ref() { - var sink = this[_fileSink]; - if (!sink) { - this.NativeWritable_lazyConstruct(); - } + const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); sink.ref(); + return this; }; NativeWritable.prototype.unref = function unref() { - this[_fileSink]?.unref(); + const sink = (this[_fileSink] ||= NativeWritable_lazyConstruct(this)); + sink.unref(); + return this; }; const exports = require_stream(); diff --git a/src/js_lexer.zig b/src/js_lexer.zig index c7adf811f11cd4..a7ac8f8bdc9400 100644 --- a/src/js_lexer.zig +++ b/src/js_lexer.zig @@ -2001,7 +2001,7 @@ fn NewLexer_( } if (comptime Environment.allow_assert) - std.debug.assert(rest.len == 0 or bun.isSliceInBuffer(u8, rest, text)); + std.debug.assert(rest.len == 0 or bun.isSliceInBuffer(rest, text)); while (rest.len > 0) { const c = rest[0]; diff --git a/src/linux_c.zig b/src/linux_c.zig index 19f289f9d58f8b..bf3091f9a2720e 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -566,13 +566,24 @@ pub extern fn vmsplice(fd: c_int, iovec: [*]const std.os.iovec, iovec_count: usi const net_c = @cImport({ @cInclude("ifaddrs.h"); // getifaddrs, freeifaddrs @cInclude("net/if.h"); // IFF_RUNNING, IFF_UP + @cInclude("fcntl.h"); // F_DUPFD_CLOEXEC + @cInclude("sys/socket.h"); }); -pub const ifaddrs = net_c.ifaddrs; -pub const getifaddrs = net_c.getifaddrs; + +pub const FD_CLOEXEC = net_c.FD_CLOEXEC; pub const freeifaddrs = net_c.freeifaddrs; +pub const getifaddrs = net_c.getifaddrs; +pub const ifaddrs = net_c.ifaddrs; +pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; pub const IFF_RUNNING = net_c.IFF_RUNNING; pub const IFF_UP = net_c.IFF_UP; -pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; +pub const MSG_DONTWAIT = net_c.MSG_DONTWAIT; +pub const MSG_NOSIGNAL = net_c.MSG_NOSIGNAL; + +pub const F = struct { + pub const DUPFD_CLOEXEC = net_c.F_DUPFD_CLOEXEC; + pub const DUPFD = net_c.F_DUPFD; +}; pub const Mode = u32; pub const E = std.os.E; @@ -596,3 +607,62 @@ pub const linux_fs = if (bun.Environment.isLinux) @cImport({ pub fn ioctl_ficlone(dest_fd: bun.FileDescriptor, srcfd: bun.FileDescriptor) usize { return std.os.linux.ioctl(dest_fd.cast(), linux_fs.FICLONE, @intCast(srcfd.int())); } + +pub const RWFFlagSupport = enum(u8) { + unknown = 0, + unsupported = 2, + supported = 1, + + var rwf_bool = std.atomic.Value(RWFFlagSupport).init(RWFFlagSupport.unknown); + + pub fn isLinuxKernelVersionWithBuggyRWF_NONBLOCK() bool { + return bun.linuxKernelVersion().major == 5 and switch (bun.linuxKernelVersion().minor) { + 9, 10 => true, + else => false, + }; + } + + pub fn disable() void { + rwf_bool.store(.unsupported, .Monotonic); + } + + /// Workaround for https://github.com/google/gvisor/issues/2601 + pub fn isMaybeSupported() bool { + if (comptime !bun.Environment.isLinux) return false; + switch (rwf_bool.load(.Monotonic)) { + .unknown => { + if (isLinuxKernelVersionWithBuggyRWF_NONBLOCK()) { + rwf_bool.store(.unsupported, .Monotonic); + return false; + } + + rwf_bool.store(.supported, .Monotonic); + return true; + }, + .supported => { + return true; + }, + else => { + return false; + }, + } + + unreachable; + } +}; + +pub extern "C" fn sys_preadv2( + fd: c_int, + iov: [*]const std.os.iovec, + iovcnt: c_int, + offset: std.os.off_t, + flags: c_uint, +) isize; + +pub extern "C" fn sys_pwritev2( + fd: c_int, + iov: [*]const std.os.iovec_const, + iovcnt: c_int, + offset: std.os.off_t, + flags: c_uint, +) isize; diff --git a/src/main.zig b/src/main.zig index 625b542fc54b4e..07075e7202d611 100644 --- a/src/main.zig +++ b/src/main.zig @@ -48,7 +48,7 @@ pub fn main() void { bun.win32.STDOUT_FD = if (stdout != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdout) else bun.invalid_fd; bun.win32.STDIN_FD = if (stdin != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdin) else bun.invalid_fd; - bun.buffered_stdin.unbuffered_reader.context.handle = stdin; + bun.Output.buffered_stdin.unbuffered_reader.context.handle = bun.win32.STDIN_FD; const w = std.os.windows; @@ -64,11 +64,16 @@ pub fn main() void { bun.start_time = std.time.nanoTimestamp(); - const stdout = std.io.getStdOut(); - const stderr = std.io.getStdErr(); + const stdout = bun.sys.File.from(std.io.getStdOut()); + const stderr = bun.sys.File.from(std.io.getStdErr()); var output_source = Output.Source.init(stdout, stderr); Output.Source.set(&output_source); + + if (comptime Environment.isDebug) { + bun.Output.initScopedDebugWriterAtStartup(); + } + defer Output.flush(); if (Environment.isX64 and Environment.enableSIMD and Environment.isPosix) { bun_warn_avx_missing(@import("./cli/upgrade_command.zig").Version.Bun__githubBaselineURL.ptr); diff --git a/src/meta.zig b/src/meta.zig index 954e77f2935519..37281e85b2832c 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -11,6 +11,11 @@ pub fn ReturnOfType(comptime Type: type) type { return typeinfo.return_type orelse void; } +pub fn typeName(comptime Type: type) []const u8 { + const name = @typeName(Type); + return typeBaseName(name); +} + // partially emulates behaviour of @typeName in previous Zig versions, // converting "some.namespace.MyType" to "MyType" pub fn typeBaseName(comptime fullname: []const u8) []const u8 { @@ -39,3 +44,13 @@ pub fn enumFieldNames(comptime Type: type) []const []const u8 { } return names[0..i]; } + +pub fn banFieldType(comptime Container: type, comptime T: type) void { + comptime { + for (std.meta.fields(Container)) |field| { + if (field.type == T) { + @compileError(std.fmt.comptimePrint(typeName(T) ++ " field \"" ++ field.name ++ "\" not allowed in " ++ typeName(Container), .{})); + } + } + } +} diff --git a/src/output.zig b/src/output.zig index 1fab443bb654ef..be5fd5956a1b97 100644 --- a/src/output.zig +++ b/src/output.zig @@ -22,7 +22,7 @@ threadlocal var source_set: bool = false; var stderr_stream: Source.StreamType = undefined; var stdout_stream: Source.StreamType = undefined; var stdout_stream_set = false; - +const File = bun.sys.File; pub var terminal_size: std.os.winsize = .{ .ws_row = 0, .ws_col = 0, @@ -35,7 +35,7 @@ pub const Source = struct { if (Environment.isWasm) { break :brk std.io.FixedBufferStream([]u8); } else { - break :brk std.fs.File; + break :brk File; // var stdout = std.io.getStdOut(); // return @TypeOf(std.io.bufferedWriter(stdout.writer())); } @@ -45,7 +45,7 @@ pub const Source = struct { if (comptime Environment.isWasm) return StreamType; - return std.io.BufferedWriter(4096, @TypeOf(StreamType.writer(undefined))); + return std.io.BufferedWriter(4096, @TypeOf(StreamType.quietWriter(undefined))); } }.getBufferedStream(); @@ -75,11 +75,11 @@ pub const Source = struct { .stream = stream, .error_stream = err_stream, .buffered_stream = if (Environment.isNative) - BufferedStream{ .unbuffered_writer = stream.writer() } + BufferedStream{ .unbuffered_writer = stream.quietWriter() } else stream, .buffered_error_stream = if (Environment.isNative) - BufferedStream{ .unbuffered_writer = err_stream.writer() } + BufferedStream{ .unbuffered_writer = err_stream.quietWriter() } else err_stream, }; @@ -213,7 +213,7 @@ pub fn initTest() void { _source_for_test_set = true; const in = std.io.getStdErr(); const out = std.io.getStdOut(); - _source_for_test = Output.Source.init(out, in); + _source_for_test = Output.Source.init(File.from(out), File.from(in)); Output.Source.set(&_source_for_test); } pub fn enableBuffering() void { @@ -235,11 +235,11 @@ pub noinline fn panic(comptime fmt: string, args: anytype) noreturn { } } -pub const WriterType: type = @TypeOf(Source.StreamType.writer(undefined)); +pub const WriterType: type = @TypeOf(Source.StreamType.quietWriter(undefined)); pub fn errorWriter() WriterType { std.debug.assert(source_set); - return source.error_stream.writer(); + return source.error_stream.quietWriter(); } pub fn errorStream() Source.StreamType { @@ -249,7 +249,7 @@ pub fn errorStream() Source.StreamType { pub fn writer() WriterType { std.debug.assert(source_set); - return source.stream.writer(); + return source.stream.quietWriter(); } pub fn resetTerminal() void { @@ -258,17 +258,17 @@ pub fn resetTerminal() void { } if (enable_ansi_colors_stderr) { - _ = source.error_stream.write("\x1b[H\x1b[2J") catch 0; + _ = source.error_stream.write("\x1b[H\x1b[2J").unwrap() catch 0; } else { - _ = source.stream.write("\x1b[H\x1b[2J") catch 0; + _ = source.stream.write("\x1b[H\x1b[2J").unwrap() catch 0; } } pub fn resetTerminalAll() void { if (enable_ansi_colors_stderr) - _ = source.error_stream.write("\x1b[H\x1b[2J") catch 0; + _ = source.error_stream.write("\x1b[H\x1b[2J").unwrap() catch 0; if (enable_ansi_colors_stdout) - _ = source.stream.write("\x1b[H\x1b[2J") catch 0; + _ = source.stream.write("\x1b[H\x1b[2J").unwrap() catch 0; } /// Write buffered stdout & stderr to the terminal. @@ -442,7 +442,12 @@ pub noinline fn print(comptime fmt: string, args: anytype) callconv(std.builtin. /// To enable all logs, set the environment variable /// BUN_DEBUG_ALL=1 const _log_fn = fn (comptime fmt: string, args: anytype) void; -pub fn scoped(comptime tag: @Type(.EnumLiteral), comptime disabled: bool) _log_fn { +pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn { + const tagname = switch (@TypeOf(tag)) { + @Type(.EnumLiteral) => @tagName(tag), + []const u8 => tag, + else => @compileError("Output.scoped expected @Type(.EnumLiteral) or []const u8, you gave: " ++ @typeName(@Type(tag))), + }; if (comptime !Environment.isDebug or !Environment.isNative) { return struct { pub fn log(comptime _: string, _: anytype) void {} @@ -450,7 +455,7 @@ pub fn scoped(comptime tag: @Type(.EnumLiteral), comptime disabled: bool) _log_f } return struct { - const BufferedWriter = Source.BufferedStream; + const BufferedWriter = std.io.BufferedWriter(4096, bun.sys.File.QuietWriter); var buffered_writer: BufferedWriter = undefined; var out: BufferedWriter.Writer = undefined; var out_set = false; @@ -470,10 +475,14 @@ pub fn scoped(comptime tag: @Type(.EnumLiteral), comptime disabled: bool) _log_f return log(fmt ++ "\n", args); } + if (ScopedDebugWriter.disable_inside_log > 0) { + return; + } + if (!evaluated_disable) { evaluated_disable = true; if (bun.getenvZ("BUN_DEBUG_ALL") != null or - bun.getenvZ("BUN_DEBUG_" ++ @tagName(tag)) != null) + bun.getenvZ("BUN_DEBUG_" ++ tagname) != null) { really_disable = false; } else if (bun.getenvZ("BUN_DEBUG_QUIET_LOGS")) |val| { @@ -491,12 +500,11 @@ pub fn scoped(comptime tag: @Type(.EnumLiteral), comptime disabled: bool) _log_f out = buffered_writer.writer(); out_set = true; } - lock.lock(); defer lock.unlock(); if (Output.enable_ansi_colors_stdout and buffered_writer.unbuffered_writer.context.handle == writer().context.handle) { - out.print(comptime prettyFmt("[" ++ @tagName(tag) ++ "] " ++ fmt, true), args) catch { + out.print(comptime prettyFmt("[" ++ tagname ++ "] " ++ fmt, true), args) catch { really_disable = true; return; }; @@ -505,7 +513,7 @@ pub fn scoped(comptime tag: @Type(.EnumLiteral), comptime disabled: bool) _log_f return; }; } else { - out.print(comptime prettyFmt("[" ++ @tagName(tag) ++ "] " ++ fmt, false), args) catch { + out.print(comptime prettyFmt("[" ++ tagname ++ "] " ++ fmt, false), args) catch { really_disable = true; return; }; @@ -804,57 +812,58 @@ pub inline fn err(error_name: anytype, comptime fmt: []const u8, args: anytype) } } -fn scopedWriter() std.fs.File.Writer { - if (comptime !Environment.isDebug) { - @compileError("scopedWriter() should only be called in debug mode"); - } - - const Scoped = struct { - pub var loaded_env: ?bool = null; - pub var scoped_file_writer: std.fs.File.Writer = undefined; - pub var scoped_file_writer_lock: bun.Lock = bun.Lock.init(); - }; +const ScopedDebugWriter = struct { + pub var scoped_file_writer: File.QuietWriter = undefined; + pub threadlocal var disable_inside_log: isize = 0; +}; +pub fn disableScopedDebugWriter() void { + ScopedDebugWriter.disable_inside_log += 1; +} +pub fn enableScopedDebugWriter() void { + ScopedDebugWriter.disable_inside_log -= 1; +} +pub fn initScopedDebugWriterAtStartup() void { std.debug.assert(source_set); - Scoped.scoped_file_writer_lock.lock(); - defer Scoped.scoped_file_writer_lock.unlock(); - const use_env = Scoped.loaded_env orelse brk: { - if (bun.getenvZ("BUN_DEBUG")) |path| { - if (path.len > 0 and !strings.eql(path, "0") and !strings.eql(path, "false")) { - if (std.fs.path.dirname(path)) |dir| { - std.fs.cwd().makePath(dir) catch {}; - } - // do not use libuv through this code path, since it might not be initialized yet. - const fd = std.os.openat( - std.fs.cwd().fd, - path, - std.os.O.TRUNC | std.os.O.CREAT | std.os.O.WRONLY, - if (Environment.isWindows) 0 else 0o644, - ) catch |err_| { - // Ensure we don't panic inside panic - Scoped.loaded_env = false; - Scoped.scoped_file_writer_lock.unlock(); - Output.panic("Failed to open file for debug output: {s} ({s})", .{ @errorName(err_), path }); - }; - Scoped.scoped_file_writer = bun.toFD(fd).asFile().writer(); - Scoped.loaded_env = true; - break :brk true; + if (bun.getenvZ("BUN_DEBUG")) |path| { + if (path.len > 0 and !strings.eql(path, "0") and !strings.eql(path, "false")) { + if (std.fs.path.dirname(path)) |dir| { + std.fs.cwd().makePath(dir) catch {}; } - } - - Scoped.loaded_env = false; - break :brk false; - }; + // do not use libuv through this code path, since it might not be initialized yet. + const fd = std.os.openat( + std.fs.cwd().fd, + path, + std.os.O.CREAT | std.os.O.WRONLY, + // on windows this is u0 + if (Environment.isWindows) 0 else 0o644, + ) catch |err_| { + Output.panic("Failed to open file for debug output: {s} ({s})", .{ @errorName(err_), path }); + }; + _ = bun.sys.ftruncate(bun.toFD(fd), 0); // windows + ScopedDebugWriter.scoped_file_writer = File.from(fd).quietWriter(); + return; + } + } - if (use_env) { - return Scoped.scoped_file_writer; + ScopedDebugWriter.scoped_file_writer = source.stream.quietWriter(); +} +fn scopedWriter() File.QuietWriter { + if (comptime !Environment.isDebug) { + @compileError("scopedWriter() should only be called in debug mode"); } - return source.stream.writer(); + return ScopedDebugWriter.scoped_file_writer; } /// Print a red error message with "error: " as the prefix. For custom prefixes see `err()` pub inline fn errGeneric(comptime fmt: []const u8, args: anytype) void { prettyErrorln("error: " ++ fmt, args); } + +/// This struct is a workaround a Windows terminal bug. +/// TODO: when https://github.com/microsoft/terminal/issues/16606 is resolved, revert this commit. +pub var buffered_stdin = std.io.BufferedReader(4096, File.Reader){ + .unbuffered_reader = File.Reader{ .context = .{ .handle = if (Environment.isWindows) undefined else bun.toFD(0) } }, +}; diff --git a/src/panic_handler.zig b/src/panic_handler.zig index 5c3c216cb97f94..2f73aeb40dce2b 100644 --- a/src/panic_handler.zig +++ b/src/panic_handler.zig @@ -29,6 +29,7 @@ pub fn NewPanicHandler(comptime panic_func: fn ([]const u8, ?*std.builtin.StackT }; } pub inline fn handle_panic(msg: []const u8, error_return_type: ?*std.builtin.StackTrace, addr: ?usize) noreturn { + // This exists to ensure we flush all buffered output before panicking. Output.flush(); diff --git a/src/report.zig b/src/report.zig index 24e07caa74aab5..60dfb0b905ab61 100644 --- a/src/report.zig +++ b/src/report.zig @@ -76,7 +76,7 @@ pub const CrashReportWriter = struct { _ = bun.sys.mkdirA(dirname, 0); } - const call = bun.sys.open(file_path, std.os.O.TRUNC, 0).unwrap() catch return; + const call = bun.sys.openA(file_path, std.os.O.CREAT | std.os.O.TRUNC, 0).unwrap() catch return; var file = call.asFile(); this.file = std.io.bufferedWriter( file.writer(), @@ -107,7 +107,11 @@ pub const CrashReportWriter = struct { pub fn printMetadata() void { @setCold(true); - crash_report_writer.generateFile(); + + if (comptime !Environment.isWindows) { + // TODO(@paperdave): report files do not work on windows, and report files in general are buggy + crash_report_writer.generateFile(); + } const cmd_label: string = if (CLI.cmd) |tag| @tagName(tag) else "Unknown"; @@ -179,7 +183,9 @@ pub fn fatal(err_: ?anyerror, msg_: ?string) void { const had_printed_fatal = has_printed_fatal; if (!has_printed_fatal) { has_printed_fatal = true; - crash_report_writer.generateFile(); + if (comptime !Environment.isWindows) { + crash_report_writer.generateFile(); + } if (err_) |err| { if (Output.isEmojiEnabled()) { @@ -234,19 +240,11 @@ pub fn fatal(err_: ?anyerror, msg_: ?string) void { crash_report_writer.flush(); - // TODO(@paperdave): - // Bun__crashReportDumpStackTrace does not work on Windows, even in a debug build - // It is fine to skip this because in release we ship with ReleaseSafe - // because zig's panic handler will also trigger right after - if (!Environment.isWindows) { - // It only is a real crash report if it's not coming from Zig - if (comptime !@import("root").bun.JSC.is_bindgen) { - std.mem.doNotOptimizeAway(&Bun__crashReportWrite); - Bun__crashReportDumpStackTrace(&crash_report_writer); - } + // It only is a real crash report if it's not coming from Zig + std.mem.doNotOptimizeAway(&Bun__crashReportWrite); + Bun__crashReportDumpStackTrace(&crash_report_writer); - crash_report_writer.flush(); - } + crash_report_writer.flush(); crash_report_writer.printPath(); } @@ -289,7 +287,10 @@ pub noinline fn handleCrash(signal: i32, addr: usize) void { if (has_printed_fatal) return; has_printed_fatal = true; - crash_report_writer.generateFile(); + if (comptime !Environment.isWindows) { + // TODO(@paperdave): report files do not work on windows, and report files in general are buggy + crash_report_writer.generateFile(); + } const name = switch (signal) { std.os.SIG.SEGV => error.SegmentationFault, diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 5f03cd812913ee..fefa000e805b5e 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -9,6 +9,18 @@ const Fs = @import("../fs.zig"); threadlocal var parser_join_input_buffer: [4096]u8 = undefined; threadlocal var parser_buffer: [1024]u8 = undefined; +pub fn z(input: []const u8, output: *[bun.MAX_PATH_BYTES]u8) [:0]const u8 { + if (input.len > bun.MAX_PATH_BYTES) { + if (comptime bun.Environment.allow_assert) @panic("path too long"); + return ""; + } + + @memcpy(output[0..input.len], input); + output[input.len] = 0; + + return output[0..input.len :0]; +} + inline fn nqlAtIndex(comptime string_count: comptime_int, index: usize, input: []const []const u8) bool { comptime var string_index = 1; @@ -1182,7 +1194,7 @@ pub fn joinZ(_parts: anytype, comptime _platform: Platform) [:0]const u8 { pub fn joinZBuf(buf: []u8, _parts: anytype, comptime _platform: Platform) [:0]const u8 { const joined = joinStringBuf(buf[0 .. buf.len - 1], _parts, _platform); - std.debug.assert(bun.isSliceInBuffer(u8, joined, buf)); + std.debug.assert(bun.isSliceInBuffer(joined, buf)); const start_offset = @intFromPtr(joined.ptr) - @intFromPtr(buf.ptr); buf[joined.len + start_offset] = 0; return buf[start_offset..][0..joined.len :0]; diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 866675a474684f..463fb5c18bb67b 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -36,10 +36,11 @@ const DirIterator = @import("../bun.js/node/dir_iterator.zig"); const CodepointIterator = @import("../string_immutable.zig").PackedCodepointIterator; const isAllAscii = @import("../string_immutable.zig").isAllASCII; const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; -// const Subprocess = bun.ShellSubprocess; const TaggedPointer = @import("../tagged_pointer.zig").TaggedPointer; pub const WorkPoolTask = @import("../work_pool.zig").Task; pub const WorkPool = @import("../work_pool.zig").WorkPool; +const windows = bun.windows; +const uv = windows.libuv; const Maybe = JSC.Maybe; const Pipe = [2]bun.FileDescriptor; @@ -72,7 +73,7 @@ pub fn assert(cond: bool, comptime msg: []const u8) void { } } -const ExitCode = if (bun.Environment.isWindows) u16 else u8; +const ExitCode = if (bun.Environment.isWindows) u16 else u16; pub const StateKind = enum(u8) { script, @@ -139,6 +140,75 @@ pub fn Cow(comptime T: type, comptime VTable: type) type { }; } +/// Copy-on-write file descriptor. This is to avoid having multiple non-blocking +/// writers to the same file descriptor, which breaks epoll/kqueue +/// +/// Two main fields: +/// 1. refcount - tracks number of references to the fd, closes file descriptor when reaches 0 +/// 2. being_written - if the fd is currently being used by a BufferedWriter for non-blocking writes +/// +/// If you want to write to the file descriptor, you call `.write()`, if `being_written` is true it will duplicate the file descriptor. +const CowFd = struct { + __fd: bun.FileDescriptor, + refcount: u32 = 1, + being_used: bool = false, + + const print = bun.Output.scoped(.CowFd, false); + + pub fn init(fd: bun.FileDescriptor) *CowFd { + const this = bun.default_allocator.create(CowFd) catch bun.outOfMemory(); + this.* = .{ + .__fd = fd, + }; + print("init(0x{x}, fd={})", .{ @intFromPtr(this), fd }); + return this; + } + + pub fn dup(this: *CowFd) Maybe(*CowFd) { + const new = bun.new(CowFd, .{ + .fd = bun.sys.dup(this.fd), + .writercount = 1, + }); + print("dup(0x{x}, fd={}) = (0x{x}, fd={})", .{ @intFromPtr(this), this.fd, new, new.fd }); + return new; + } + + pub fn use(this: *CowFd) Maybe(*CowFd) { + if (!this.being_used) { + this.being_used = true; + this.ref(); + return .{ .result = this }; + } + return this.dup(); + } + + pub fn doneUsing(this: *CowFd) void { + this.being_used = false; + } + + pub fn ref(this: *CowFd) void { + this.refcount += 1; + } + + pub fn refSelf(this: *CowFd) *CowFd { + this.ref(); + return this; + } + + pub fn deref(this: *CowFd) void { + this.refcount -= 1; + if (this.refcount == 0) { + this.deinit(); + } + } + + pub fn deinit(this: *CowFd) void { + std.debug.assert(this.refcount == 0); + _ = bun.sys.close(this.__fd); + bun.default_allocator.destroy(this); + } +}; + pub const CoroutineResult = enum { /// it's okay for the caller to continue its execution cont, @@ -146,18 +216,78 @@ pub const CoroutineResult = enum { }; pub const IO = struct { - stdin: Kind = .{ .std = .{} }, - stdout: Kind = .{ .std = .{} }, - stderr: Kind = .{ .std = .{} }, + stdin: InKind, + stdout: OutKind, + stderr: OutKind, + + pub fn deinit(this: *IO) void { + this.stdin.close(); + this.stdout.close(); + this.stderr.close(); + } + + pub fn copy(this: *IO) IO { + _ = this.ref(); + return this.*; + } + + pub fn ref(this: *IO) *IO { + _ = this.stdin.ref(); + _ = this.stdout.ref(); + _ = this.stderr.ref(); + return this; + } + + pub fn deref(this: *IO) void { + this.stdin.deref(); + this.stdout.deref(); + this.stderr.deref(); + } + + pub const InKind = union(enum) { + fd: *Interpreter.IOReader, + ignore, + + pub fn ref(this: InKind) InKind { + switch (this) { + .fd => this.fd.ref(), + .ignore => {}, + } + return this; + } + + pub fn deref(this: InKind) void { + switch (this) { + .fd => this.fd.deref(), + .ignore => {}, + } + } + + pub fn close(this: InKind) void { + switch (this) { + .fd => this.fd.deref(), + .ignore => {}, + } + } + + pub fn to_subproc_stdio(this: InKind, stdio: *bun.shell.subproc.Stdio) void { + switch (this) { + .fd => { + stdio.* = .{ .fd = this.fd.fd }; + }, + .ignore => { + stdio.* = .ignore; + }, + } + } + }; - pub const Kind = union(enum) { - /// Use stdin/stdout/stderr of this process + pub const OutKind = union(enum) { + /// Write/Read to/from file descriptor /// If `captured` is non-null, it will write to std{out,err} and also buffer it. /// The pointer points to the `buffered_stdout`/`buffered_stdin` fields /// in the Interpreter struct - std: struct { captured: ?*bun.ByteList = null }, - /// Write/Read to/from file descriptor - fd: bun.FileDescriptor, + fd: struct { writer: *Interpreter.IOWriter, captured: ?*bun.ByteList = null }, /// Buffers the output (handled in Cmd.BufferedIoClosed.close()) pipe, /// Discards output @@ -165,35 +295,57 @@ pub const IO = struct { // fn dupeForSubshell(this: *ShellState, - fn close(this: Kind) void { + pub fn ref(this: @This()) @This() { + switch (this) { + .fd => { + this.fd.writer.ref(); + }, + else => {}, + } + return this; + } + + pub fn deref(this: @This()) void { + this.close(); + } + + pub fn enqueueFmtBltn( + this: *@This(), + ptr: anytype, + comptime kind: ?Interpreter.Builtin.Kind, + comptime fmt_: []const u8, + args: anytype, + ) void { + if (bun.Environment.allow_assert) std.debug.assert(this.* == .fd); + this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args); + } + + fn close(this: OutKind) void { switch (this) { .fd => { - closefd(this.fd); + this.fd.writer.deref(); }, else => {}, } } - fn to_subproc_stdio(this: Kind) bun.shell.subproc.Stdio { + fn to_subproc_stdio(this: OutKind) bun.shell.subproc.Stdio { return switch (this) { - .std => .{ .inherit = .{ .captured = this.std.captured } }, - .fd => |val| .{ .fd = val }, - .pipe => .{ .pipe = null }, + .fd => |val| if (val.captured) |cap| .{ .capture = .{ .buf = cap, .fd = val.writer.fd } } else .{ .fd = val.writer.fd }, + .pipe => .pipe, .ignore => .ignore, }; } }; fn to_subproc_stdio(this: IO, stdio: *[3]bun.shell.subproc.Stdio) void { - stdio[stdin_no] = this.stdin.to_subproc_stdio(); + // stdio[stdin_no] = this.stdin.to_subproc_stdio(); + this.stdin.to_subproc_stdio(&stdio[0]); stdio[stdout_no] = this.stdout.to_subproc_stdio(); stdio[stderr_no] = this.stderr.to_subproc_stdio(); } }; -pub const Interpreter = NewInterpreter(.js); -pub const InterpreterMini = NewInterpreter(.mini); - /// Environment strings need to be copied a lot /// So we make them reference counted /// @@ -356,6 +508,12 @@ pub const EnvMap = struct { return .{ .map = MapType.init(alloc) }; } + fn initWithCapacity(alloc: Allocator, cap: usize) EnvMap { + var map = MapType.init(alloc); + map.ensureTotalCapacity(cap) catch bun.outOfMemory(); + return .{ .map = map }; + } + fn deinit(this: *EnvMap) void { this.derefStrings(); this.map.deinit(); @@ -427,7198 +585,8658 @@ pub const EnvMap = struct { /// This interpreter works by basically turning the AST into a state machine so /// that execution can be suspended and resumed to support async. -pub fn NewInterpreter(comptime EventLoopKind: JSC.EventLoopKind) type { - const GlobalRef = switch (EventLoopKind) { - .js => *JSGlobalObject, - .mini => *JSC.MiniEventLoop, - }; - - const GlobalHandle = switch (EventLoopKind) { - .js => bun.shell.GlobalJS, - .mini => bun.shell.GlobalMini, - }; - - const EventLoopRef = switch (EventLoopKind) { - .js => *JSC.EventLoop, - .mini => *JSC.MiniEventLoop, - }; - const event_loop_ref = struct { - fn get() EventLoopRef { - return switch (EventLoopKind) { - .js => JSC.VirtualMachine.get().event_loop, - .mini => bun.JSC.MiniEventLoop.global, - }; - } - }; - const global_handle = struct { - fn get() GlobalHandle { - return switch (EventLoopKind) { - .js => bun.shell.GlobalJS.init(JSC.VirtualMachine.get().global), - .mini => bun.shell.GlobalMini.init(bun.JSC.MiniEventLoop.global), - }; - } - }; - - const EventLoopTask = switch (EventLoopKind) { - .js => JSC.ConcurrentTask, - .mini => JSC.AnyTaskWithExtraContext, - }; +pub const Interpreter = struct { + event_loop: JSC.EventLoopHandle, + /// This is the arena used to allocate the input shell script's AST nodes, + /// tokens, and a string pool used to store all strings. + arena: bun.ArenaAllocator, + /// This is the allocator used to allocate interpreter state + allocator: Allocator, - // const Builtin = switch (EventLoopKind) { - // .js => NewBuiltin(.js), - // .mini => NewBuiltin(.mini), - // }; + /// Root ast node + script: *ast.Script, - // const Subprocess = switch (EventLoopKind) { - // .js => bun.shell.Subprocess, - // .mini => bun.shell.SubprocessMini, - // }; - // const Subprocess = bun.shell.subproc.NewShellSubprocess(EventLoopKind); + /// JS objects used as input for the shell script + /// This should be allocated using the arena + jsobjs: []JSValue, - return struct { - global: GlobalRef, - /// This is the arena used to allocate the input shell script's AST nodes, - /// tokens, and a string pool used to store all strings. - arena: bun.ArenaAllocator, - /// This is the allocator used to allocate interpreter state - allocator: Allocator, + root_shell: ShellState, + root_io: IO, - /// Root ast node - script: *ast.Script, + resolve: JSC.Strong = .{}, + reject: JSC.Strong = .{}, + has_pending_activity: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), + started: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), - /// JS objects used as input for the shell script - /// This should be allocated using the arena - jsobjs: []JSValue, + done: ?*bool = null, - root_shell: ShellState, + const InterpreterChildPtr = StatePtrUnion(.{ + Script, + }); - resolve: JSC.Strong = .{}, - reject: JSC.Strong = .{}, - has_pending_activity: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), - started: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + pub const ShellState = struct { + kind: Kind = .normal, - done: ?*bool = null, + /// This is the buffered stdout/stderr that captures the entire + /// output of the script and is given to JS. + /// + /// Accross the entire script execution, this is usually the same. + /// + /// It changes when a cmd substitution is run. + /// + /// These MUST use the `bun.default_allocator` Allocator + _buffered_stdout: Bufio = .{ .owned = .{} }, + _buffered_stderr: Bufio = .{ .owned = .{} }, + + /// TODO Performance optimization: make these env maps copy-on-write + /// Shell env for expansion by the shell + shell_env: EnvMap, + /// Local environment variables to be given to a subprocess + cmd_local_env: EnvMap, + /// Exported environment variables available to all subprocesses. This includes system ones. + export_env: EnvMap, + + /// The current working directory of the shell. + /// Use an array list so we don't have to keep reallocating + /// Always has zero-sentinel + __prev_cwd: std.ArrayList(u8), + __cwd: std.ArrayList(u8), + cwd_fd: bun.FileDescriptor, + + const Bufio = union(enum) { owned: bun.ByteList, borrowed: *bun.ByteList }; + + const Kind = enum { + normal, + cmd_subst, + subshell, + pipeline, + }; - const InterpreterChildPtr = StatePtrUnion(.{ - Script, - }); + pub fn buffered_stdout(this: *ShellState) *bun.ByteList { + return switch (this._buffered_stdout) { + .owned => &this._buffered_stdout.owned, + .borrowed => this._buffered_stdout.borrowed, + }; + } - pub const ShellState = struct { - io: IO = .{}, - kind: Kind = .normal, - - /// These MUST use the `bun.default_allocator` Allocator - _buffered_stdout: Bufio = .{ .owned = .{} }, - _buffered_stderr: Bufio = .{ .owned = .{} }, - - /// TODO Performance optimization: make these env maps copy-on-write - /// Shell env for expansion by the shell - shell_env: EnvMap, - /// Local environment variables to be given to a subprocess - cmd_local_env: EnvMap, - /// Exported environment variables available to all subprocesses. This includes system ones. - export_env: EnvMap, - - /// The current working directory of the shell. - /// Use an array list so we don't have to keep reallocating - /// Always has zero-sentinel - __prev_cwd: std.ArrayList(u8), - __cwd: std.ArrayList(u8), - cwd_fd: bun.FileDescriptor, - - const Bufio = union(enum) { owned: bun.ByteList, borrowed: *bun.ByteList }; - - const Kind = enum { - normal, - cmd_subst, - subshell, - pipeline, + pub fn buffered_stderr(this: *ShellState) *bun.ByteList { + return switch (this._buffered_stderr) { + .owned => &this._buffered_stderr.owned, + .borrowed => this._buffered_stderr.borrowed, }; + } - pub fn buffered_stdout(this: *ShellState) *bun.ByteList { - return switch (this._buffered_stdout) { - .owned => &this._buffered_stdout.owned, - .borrowed => this._buffered_stdout.borrowed, - }; - } + pub inline fn cwdZ(this: *ShellState) [:0]const u8 { + if (this.__cwd.items.len == 0) return ""; + return this.__cwd.items[0..this.__cwd.items.len -| 1 :0]; + } - pub fn buffered_stderr(this: *ShellState) *bun.ByteList { - return switch (this._buffered_stderr) { - .owned => &this._buffered_stderr.owned, - .borrowed => this._buffered_stderr.borrowed, - }; - } + pub inline fn prevCwdZ(this: *ShellState) [:0]const u8 { + if (this.__prev_cwd.items.len == 0) return ""; + return this.__prev_cwd.items[0..this.__prev_cwd.items.len -| 1 :0]; + } - pub inline fn cwdZ(this: *ShellState) [:0]const u8 { - if (this.__cwd.items.len == 0) return ""; - return this.__cwd.items[0..this.__cwd.items.len -| 1 :0]; - } + pub inline fn prevCwd(this: *ShellState) []const u8 { + const prevcwdz = this.prevCwdZ(); + return prevcwdz[0..prevcwdz.len]; + } - pub inline fn prevCwdZ(this: *ShellState) [:0]const u8 { - if (this.__prev_cwd.items.len == 0) return ""; - return this.__prev_cwd.items[0..this.__prev_cwd.items.len -| 1 :0]; - } + pub inline fn cwd(this: *ShellState) []const u8 { + const cwdz = this.cwdZ(); + return cwdz[0..cwdz.len]; + } - pub inline fn prevCwd(this: *ShellState) []const u8 { - const prevcwdz = this.prevCwdZ(); - return prevcwdz[0..prevcwdz.len]; - } + pub fn deinit(this: *ShellState) void { + this.deinitImpl(true, true); + } - pub inline fn cwd(this: *ShellState) []const u8 { - const cwdz = this.cwdZ(); - return cwdz[0..cwdz.len]; - } + /// If called by interpreter we have to: + /// 1. not free this *ShellState, because its on a field on the interpreter + /// 2. don't free buffered_stdout and buffered_stderr, because that is used for output + fn deinitImpl(this: *ShellState, comptime destroy_this: bool, comptime free_buffered_io: bool) void { + log("[ShellState] deinit {x}", .{@intFromPtr(this)}); - pub fn deinit(this: *ShellState) void { - this.deinitImpl(true, true); + if (comptime free_buffered_io) { + if (this._buffered_stdout == .owned) { + this._buffered_stdout.owned.deinitWithAllocator(bun.default_allocator); + } + if (this._buffered_stderr == .owned) { + this._buffered_stderr.owned.deinitWithAllocator(bun.default_allocator); + } } - /// If called by interpreter we have to: - /// 1. not free this *ShellState, because its on a field on the interpreter - /// 2. don't free buffered_stdout and buffered_stderr, because that is used for output - fn deinitImpl(this: *ShellState, comptime destroy_this: bool, comptime free_buffered_io: bool) void { - log("[ShellState] deinit {x}", .{@intFromPtr(this)}); - - if (comptime free_buffered_io) { - if (this._buffered_stdout == .owned) { - this._buffered_stdout.owned.deinitWithAllocator(bun.default_allocator); - } - if (this._buffered_stderr == .owned) { - this._buffered_stderr.owned.deinitWithAllocator(bun.default_allocator); - } - } + // this.io.deinit(); + this.shell_env.deinit(); + this.cmd_local_env.deinit(); + this.export_env.deinit(); + this.__cwd.deinit(); + this.__prev_cwd.deinit(); + closefd(this.cwd_fd); - this.shell_env.deinit(); - this.cmd_local_env.deinit(); - this.export_env.deinit(); - this.__cwd.deinit(); - this.__prev_cwd.deinit(); - closefd(this.cwd_fd); + if (comptime destroy_this) bun.default_allocator.destroy(this); + } - if (comptime destroy_this) bun.default_allocator.destroy(this); - } + pub fn dupeForSubshell(this: *ShellState, allocator: Allocator, io: IO, kind: Kind) Maybe(*ShellState) { + const duped = allocator.create(ShellState) catch bun.outOfMemory(); - pub fn dupeForSubshell(this: *ShellState, allocator: Allocator, io: IO, kind: Kind) Maybe(*ShellState) { - const duped = allocator.create(ShellState) catch bun.outOfMemory(); + const dupedfd = switch (Syscall.dup(this.cwd_fd)) { + .err => |err| return .{ .err = err }, + .result => |fd| fd, + }; - const dupedfd = switch (Syscall.dup(this.cwd_fd)) { - .err => |err| return .{ .err = err }, - .result => |fd| fd, - }; + const stdout: Bufio = if (io.stdout == .fd) brk: { + if (io.stdout.fd.captured != null) break :brk .{ .borrowed = io.stdout.fd.captured.? }; + break :brk .{ .owned = .{} }; + } else if (kind == .pipeline) .{ .borrowed = this.buffered_stdout() } else .{ .owned = .{} }; + + const stderr: Bufio = if (io.stderr == .fd) brk: { + if (io.stderr.fd.captured != null) break :brk .{ .borrowed = io.stderr.fd.captured.? }; + break :brk .{ .owned = .{} }; + } else if (kind == .pipeline) .{ .borrowed = this.buffered_stderr() } else .{ .owned = .{} }; + + duped.* = .{ + .kind = kind, + ._buffered_stdout = stdout, + ._buffered_stderr = stderr, + .shell_env = this.shell_env.clone(), + .cmd_local_env = EnvMap.init(allocator), + .export_env = this.export_env.clone(), + + .__prev_cwd = this.__prev_cwd.clone() catch bun.outOfMemory(), + .__cwd = this.__cwd.clone() catch bun.outOfMemory(), + // TODO probably need to use os.dup here + .cwd_fd = dupedfd, + }; - const stdout: Bufio = if (io.stdout == .std) brk: { - if (io.stdout.std.captured != null) break :brk .{ .borrowed = io.stdout.std.captured.? }; - break :brk .{ .owned = .{} }; - } else if (kind == .pipeline) - .{ .borrowed = this.buffered_stdout() } - else - .{ .owned = .{} }; - - const stderr: Bufio = if (io.stderr == .std) brk: { - if (io.stderr.std.captured != null) break :brk .{ .borrowed = io.stderr.std.captured.? }; - break :brk .{ .owned = .{} }; - } else if (kind == .pipeline) - .{ .borrowed = this.buffered_stderr() } - else - .{ .owned = .{} }; - - duped.* = .{ - .io = io, - .kind = kind, - ._buffered_stdout = stdout, - ._buffered_stderr = stderr, - .shell_env = this.shell_env.clone(), - .cmd_local_env = EnvMap.init(allocator), - .export_env = this.export_env.clone(), - - .__prev_cwd = this.__prev_cwd.clone() catch bun.outOfMemory(), - .__cwd = this.__cwd.clone() catch bun.outOfMemory(), - // TODO probably need to use os.dup here - .cwd_fd = dupedfd, - }; + return .{ .result = duped }; + } - return .{ .result = duped }; + pub fn assignVar(this: *ShellState, interp: *ThisInterpreter, label: EnvStr, value: EnvStr, assign_ctx: AssignCtx) void { + _ = interp; // autofix + switch (assign_ctx) { + .cmd => this.cmd_local_env.insert(label, value), + .shell => this.shell_env.insert(label, value), + .exported => this.export_env.insert(label, value), } + } - pub fn assignVar(this: *ShellState, interp: *ThisInterpreter, label: EnvStr, value: EnvStr, assign_ctx: AssignCtx) void { - _ = interp; // autofix - switch (assign_ctx) { - .cmd => this.cmd_local_env.insert(label, value), - .shell => this.shell_env.insert(label, value), - .exported => this.export_env.insert(label, value), - } - } + pub fn changePrevCwd(self: *ShellState, interp: *ThisInterpreter) Maybe(void) { + return self.changeCwd(interp, self.prevCwdZ()); + } - pub fn changePrevCwd(self: *ShellState, interp: *ThisInterpreter) Maybe(void) { - return self.changeCwd(interp, self.prevCwdZ()); + // pub fn changeCwd(this: *ShellState, interp: *ThisInterpreter, new_cwd_: [:0]const u8) Maybe(void) { + pub fn changeCwd(this: *ShellState, interp: *ThisInterpreter, new_cwd_: anytype) Maybe(void) { + _ = interp; // autofix + if (comptime @TypeOf(new_cwd_) != [:0]const u8 and @TypeOf(new_cwd_) != []const u8) { + @compileError("Bad type for new_cwd " ++ @typeName(@TypeOf(new_cwd_))); } + const is_sentinel = @TypeOf(new_cwd_) == [:0]const u8; - // pub fn changeCwd(this: *ShellState, interp: *ThisInterpreter, new_cwd_: [:0]const u8) Maybe(void) { - pub fn changeCwd(this: *ShellState, interp: *ThisInterpreter, new_cwd_: anytype) Maybe(void) { - _ = interp; // autofix - if (comptime @TypeOf(new_cwd_) != [:0]const u8 and @TypeOf(new_cwd_) != []const u8) { - @compileError("Bad type for new_cwd " ++ @typeName(@TypeOf(new_cwd_))); - } - const is_sentinel = @TypeOf(new_cwd_) == [:0]const u8; - - const new_cwd: [:0]const u8 = brk: { - if (ResolvePath.Platform.auto.isAbsolute(new_cwd_)) { - if (is_sentinel) { - @memcpy(ResolvePath.join_buf[0..new_cwd_.len], new_cwd_[0..new_cwd_.len]); - ResolvePath.join_buf[new_cwd_.len] = 0; - break :brk ResolvePath.join_buf[0..new_cwd_.len :0]; - } - std.mem.copyForwards(u8, &ResolvePath.join_buf, new_cwd_); + const new_cwd: [:0]const u8 = brk: { + if (ResolvePath.Platform.auto.isAbsolute(new_cwd_)) { + if (is_sentinel) { + @memcpy(ResolvePath.join_buf[0..new_cwd_.len], new_cwd_[0..new_cwd_.len]); ResolvePath.join_buf[new_cwd_.len] = 0; break :brk ResolvePath.join_buf[0..new_cwd_.len :0]; } + std.mem.copyForwards(u8, &ResolvePath.join_buf, new_cwd_); + ResolvePath.join_buf[new_cwd_.len] = 0; + break :brk ResolvePath.join_buf[0..new_cwd_.len :0]; + } - const existing_cwd = this.cwd(); - const cwd_str = ResolvePath.joinZ(&[_][]const u8{ - existing_cwd, - new_cwd_, - }, .auto); + const existing_cwd = this.cwd(); + const cwd_str = ResolvePath.joinZ(&[_][]const u8{ + existing_cwd, + new_cwd_, + }, .auto); - // remove trailing separator - if (cwd_str.len > 1 and cwd_str[cwd_str.len - 1] == '/') { + // remove trailing separator + if (bun.Environment.isWindows) { + const sep = '\\'; + if (cwd_str.len > 1 and cwd_str[cwd_str.len - 1] == sep) { ResolvePath.join_buf[cwd_str.len - 1] = 0; break :brk ResolvePath.join_buf[0 .. cwd_str.len - 1 :0]; } + } + if (cwd_str.len > 1 and cwd_str[cwd_str.len - 1] == '/') { + ResolvePath.join_buf[cwd_str.len - 1] = 0; + break :brk ResolvePath.join_buf[0 .. cwd_str.len - 1 :0]; + } - break :brk cwd_str; - }; + break :brk cwd_str; + }; - const new_cwd_fd = switch (Syscall.openat( - this.cwd_fd, - new_cwd, - std.os.O.DIRECTORY | std.os.O.RDONLY, - 0, - )) { - .result => |fd| fd, - .err => |err| { - return Maybe(void).initErr(err); - }, - }; - _ = Syscall.close2(this.cwd_fd); + const new_cwd_fd = switch (ShellSyscall.openat( + this.cwd_fd, + new_cwd, + std.os.O.DIRECTORY | std.os.O.RDONLY, + 0, + )) { + .result => |fd| fd, + .err => |err| { + return Maybe(void).initErr(err); + }, + }; + _ = Syscall.close2(this.cwd_fd); - this.__prev_cwd.clearRetainingCapacity(); - this.__prev_cwd.appendSlice(this.__cwd.items[0..]) catch bun.outOfMemory(); + this.__prev_cwd.clearRetainingCapacity(); + this.__prev_cwd.appendSlice(this.__cwd.items[0..]) catch bun.outOfMemory(); - this.__cwd.clearRetainingCapacity(); - this.__cwd.appendSlice(new_cwd[0 .. new_cwd.len + 1]) catch bun.outOfMemory(); + this.__cwd.clearRetainingCapacity(); + this.__cwd.appendSlice(new_cwd[0 .. new_cwd.len + 1]) catch bun.outOfMemory(); - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.__cwd.items[this.__cwd.items.len -| 1] == 0); - std.debug.assert(this.__prev_cwd.items[this.__prev_cwd.items.len -| 1] == 0); - } + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.__cwd.items[this.__cwd.items.len -| 1] == 0); + std.debug.assert(this.__prev_cwd.items[this.__prev_cwd.items.len -| 1] == 0); + } - this.cwd_fd = new_cwd_fd; + this.cwd_fd = new_cwd_fd; - this.export_env.insert(EnvStr.initSlice("OLDPWD"), EnvStr.initSlice(this.prevCwd())); - this.export_env.insert(EnvStr.initSlice("PWD"), EnvStr.initSlice(this.cwd())); + this.export_env.insert(EnvStr.initSlice("OLDPWD"), EnvStr.initSlice(this.prevCwd())); + this.export_env.insert(EnvStr.initSlice("PWD"), EnvStr.initSlice(this.cwd())); - return Maybe(void).success; - } + return Maybe(void).success; + } - pub fn getHomedir(self: *ShellState) EnvStr { - if (comptime bun.Environment.isWindows) { - if (self.export_env.get(EnvStr.initSlice("USERPROFILE"))) |env| { - env.ref(); - return env; - } - } else { - if (self.export_env.get(EnvStr.initSlice("HOME"))) |env| { - env.ref(); - return env; - } + pub fn getHomedir(self: *ShellState) EnvStr { + if (comptime bun.Environment.isWindows) { + if (self.export_env.get(EnvStr.initSlice("USERPROFILE"))) |env| { + env.ref(); + return env; } - return EnvStr.initSlice("unknown"); - } - - pub fn writeFailingError( - this: *ShellState, - buf: []const u8, - ctx: anytype, - comptime handleIOWrite: fn ( - c: @TypeOf(ctx), - bufw: BufferedWriter, - ) void, - ) CoroutineResult { - const IOWriteFn = struct { - pub fn run(c: @TypeOf(ctx), bufw: BufferedWriter) void { - handleIOWrite(c, bufw); - } - }; - - switch (this.writeIO(.stderr, buf, ctx, IOWriteFn.run)) { - .cont => { - ctx.parent.childDone(ctx, 1); - return .yield; - }, - .yield => return .yield, - } - } - - pub fn writeIO( - this: *ShellState, - comptime iotype: @Type(.EnumLiteral), - buf: []const u8, - ctx: anytype, - comptime handleIOWrite: fn ( - c: @TypeOf(ctx), - bufw: BufferedWriter, - ) void, - ) CoroutineResult { - const io: *IO.Kind = &@field(this.io, @tagName(iotype)); - - switch (io.*) { - .std => |val| { - const bw = BufferedWriter{ - .fd = if (iotype == .stdout) bun.STDOUT_FD else bun.STDERR_FD, - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(ctx), - .bytelist = val.captured, - }; - handleIOWrite(ctx, bw); - return .yield; - }, - .fd => { - const bw = BufferedWriter{ - .fd = if (iotype == .stdout) bun.STDOUT_FD else bun.STDERR_FD, - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(ctx), - }; - handleIOWrite(ctx, bw); - return .yield; - }, - .pipe => { - const func = @field(ShellState, "buffered_" ++ @tagName(iotype)); - const bufio: *bun.ByteList = func(this); - bufio.append(bun.default_allocator, buf) catch bun.outOfMemory(); - // this.parent.childDone(this, 1); - return .cont; - }, - .ignore => { - // this.parent.childDone(this, 1); - return .cont; - }, + } else { + if (self.export_env.get(EnvStr.initSlice("HOME"))) |env| { + env.ref(); + return env; } } - }; + return EnvStr.initSlice("unknown"); + } + + pub fn writeFailingErrorFmt( + this: *ShellState, + ctx: anytype, + enqueueCb: fn (c: @TypeOf(ctx)) void, + comptime fmt: []const u8, + args: anytype, + ) void { + const io: *IO.OutKind = &@field(ctx.io, "stderr"); + switch (io.*) { + .fd => |x| { + enqueueCb(ctx); + x.writer.enqueueFmt(ctx, x.captured, fmt, args); + }, + .pipe => { + const bufio: *bun.ByteList = this.buffered_stderr(); + bufio.appendFmt(bun.default_allocator, fmt, args) catch bun.outOfMemory(); + ctx.parent.childDone(ctx, 1); + }, + .ignore => {}, + } + } + }; - pub usingnamespace JSC.Codegen.JSShellInterpreter; + pub usingnamespace JSC.Codegen.JSShellInterpreter; - const ThisInterpreter = @This(); + const ThisInterpreter = @This(); - const ShellErrorKind = error{ - OutOfMemory, - Syscall, - }; + const ShellErrorKind = error{ + OutOfMemory, + Syscall, + }; - const ShellErrorCtx = union(enum) { - syscall: Syscall.Error, - other: ShellErrorKind, + const ShellErrorCtx = union(enum) { + syscall: Syscall.Error, + other: ShellErrorKind, - fn toJSC(this: ShellErrorCtx, globalThis: *JSGlobalObject) JSValue { - return switch (this) { - .syscall => |err| err.toJSC(globalThis), - .other => |err| bun.JSC.ZigString.fromBytes(@errorName(err)).toValueGC(globalThis), - }; - } + fn toJSC(this: ShellErrorCtx, globalThis: *JSGlobalObject) JSValue { + return switch (this) { + .syscall => |err| err.toJSC(globalThis), + .other => |err| bun.JSC.ZigString.fromBytes(@errorName(err)).toValueGC(globalThis), + }; + } + }; + + pub fn constructor( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) ?*ThisInterpreter { + const allocator = bun.default_allocator; + var arena = bun.ArenaAllocator.init(allocator); + + const arguments_ = callframe.arguments(1); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + const string_args = arguments.nextEat() orelse { + globalThis.throw("shell: expected 2 arguments, got 0", .{}); + return null; }; - pub fn constructor( - globalThis: *JSC.JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) ?*ThisInterpreter { - const allocator = bun.default_allocator; - var arena = bun.ArenaAllocator.init(allocator); - - const arguments_ = callframe.arguments(1); - var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); - const string_args = arguments.nextEat() orelse { - globalThis.throw("shell: expected 2 arguments, got 0", .{}); - return null; - }; + const template_args = callframe.argumentsPtr()[1..callframe.argumentsCount()]; + var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); + var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { + globalThis.throwOutOfMemory(); + return null; + }; + defer { + for (jsstrings.items[0..]) |bunstr| { + bunstr.deref(); + } + jsstrings.deinit(); + } + var jsobjs = std.ArrayList(JSValue).init(arena.allocator()); + var script = std.ArrayList(u8).init(arena.allocator()); + if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &jsstrings, &script) catch { + globalThis.throwOutOfMemory(); + return null; + })) { + return null; + } - const template_args = callframe.argumentsPtr()[1..callframe.argumentsCount()]; - var stack_alloc = std.heap.stackFallback(@sizeOf(bun.String) * 4, arena.allocator()); - var jsstrings = std.ArrayList(bun.String).initCapacity(stack_alloc.get(), 4) catch { - globalThis.throwOutOfMemory(); + var parser: ?bun.shell.Parser = null; + var lex_result: ?shell.LexResult = null; + const script_ast = ThisInterpreter.parse( + &arena, + script.items[0..], + jsobjs.items[0..], + jsstrings.items[0..], + &parser, + &lex_result, + ) catch |err| { + if (err == shell.ParseError.Lex) { + std.debug.assert(lex_result != null); + const str = lex_result.?.combineErrors(arena.allocator()); + globalThis.throwPretty("{s}", .{str}); return null; - }; - defer { - for (jsstrings.items[0..]) |bunstr| { - bunstr.deref(); - } - jsstrings.deinit(); } - var jsobjs = std.ArrayList(JSValue).init(arena.allocator()); - var script = std.ArrayList(u8).init(arena.allocator()); - if (!(bun.shell.shellCmdFromJS(globalThis, string_args, template_args, &jsobjs, &jsstrings, &script) catch { - globalThis.throwOutOfMemory(); - return null; - })) { + + if (parser) |*p| { + const errstr = p.combineErrors(); + globalThis.throwPretty("{s}", .{errstr}); return null; } - var parser: ?bun.shell.Parser = null; - var lex_result: ?shell.LexResult = null; - const script_ast = ThisInterpreter.parse( - &arena, - script.items[0..], - jsobjs.items[0..], - jsstrings.items[0..], - &parser, - &lex_result, - ) catch |err| { - if (err == shell.ParseError.Lex) { - std.debug.assert(lex_result != null); - const str = lex_result.?.combineErrors(arena.allocator()); - globalThis.throwPretty("{s}", .{str}); - return null; - } - - if (parser) |*p| { - const errstr = p.combineErrors(); - globalThis.throwPretty("{s}", .{errstr}); - return null; - } + globalThis.throwError(err, "failed to lex/parse shell"); + return null; + }; - globalThis.throwError(err, "failed to lex/parse shell"); - return null; - }; + const script_heap = arena.allocator().create(bun.shell.AST.Script) catch { + globalThis.throwOutOfMemory(); + return null; + }; - const script_heap = arena.allocator().create(bun.shell.AST.Script) catch { - globalThis.throwOutOfMemory(); + script_heap.* = script_ast; + + const interpreter = switch (ThisInterpreter.init( + .{ .js = globalThis.bunVM().event_loop }, + allocator, + &arena, + script_heap, + jsobjs.items[0..], + )) { + .result => |i| i, + .err => |*e| { + arena.deinit(); + throwShellErr(e, .{ .js = globalThis.bunVM().event_loop }); return null; - }; - - script_heap.* = script_ast; - - const interpreter = switch (ThisInterpreter.init( - globalThis, - allocator, - &arena, - script_heap, - jsobjs.items[0..], - )) { - .result => |i| i, - .err => |e| { - arena.deinit(); - GlobalHandle.init(globalThis).actuallyThrow(e); - return null; - }, - }; + }, + }; - return interpreter; - } + return interpreter; + } - pub fn parse( - arena: *bun.ArenaAllocator, - script: []const u8, - jsobjs: []JSValue, - jsstrings_to_escape: []bun.String, - out_parser: *?bun.shell.Parser, - out_lex_result: *?shell.LexResult, - ) !ast.Script { - const lex_result = brk: { - if (bun.strings.isAllASCII(script)) { - var lexer = bun.shell.LexerAscii.new(arena.allocator(), script, jsstrings_to_escape); - try lexer.lex(); - break :brk lexer.get_result(); - } - var lexer = bun.shell.LexerUnicode.new(arena.allocator(), script, jsstrings_to_escape); + pub fn parse( + arena: *bun.ArenaAllocator, + script: []const u8, + jsobjs: []JSValue, + jsstrings_to_escape: []bun.String, + out_parser: *?bun.shell.Parser, + out_lex_result: *?shell.LexResult, + ) !ast.Script { + const lex_result = brk: { + if (bun.strings.isAllASCII(script)) { + var lexer = bun.shell.LexerAscii.new(arena.allocator(), script, jsstrings_to_escape); try lexer.lex(); break :brk lexer.get_result(); - }; - - if (lex_result.errors.len > 0) { - out_lex_result.* = lex_result; - return shell.ParseError.Lex; } + var lexer = bun.shell.LexerUnicode.new(arena.allocator(), script, jsstrings_to_escape); + try lexer.lex(); + break :brk lexer.get_result(); + }; - out_parser.* = try bun.shell.Parser.new(arena.allocator(), lex_result, jsobjs); - - const script_ast = try out_parser.*.?.parse(); - return script_ast; + if (lex_result.errors.len > 0) { + out_lex_result.* = lex_result; + return shell.ParseError.Lex; } - // fn bunStringDealloc(this: *anyopaque, str: *anyopaque, size: u32) callconv(.C) void {} + out_parser.* = try bun.shell.Parser.new(arena.allocator(), lex_result, jsobjs); - /// If all initialization allocations succeed, the arena will be copied - /// into the interpreter struct, so it is not a stale reference and safe to call `arena.deinit()` on error. - pub fn init( - global: GlobalRef, - allocator: Allocator, - arena: *bun.ArenaAllocator, - script: *ast.Script, - jsobjs: []JSValue, - ) shell.Result(*ThisInterpreter) { - var interpreter = allocator.create(ThisInterpreter) catch bun.outOfMemory(); - interpreter.global = global; - interpreter.allocator = allocator; - - const export_env = brk: { - var export_env = EnvMap.init(allocator); - // This will be set by in the shell builtin to `process.env` - if (EventLoopKind == .js) break :brk export_env; - - var env_loader: *bun.DotEnv.Loader = env_loader: { - if (comptime EventLoopKind == .js) { - break :env_loader global.bunVM().bundler.env; - } + const script_ast = try out_parser.*.?.parse(); + return script_ast; + } - break :env_loader global.env.?; - }; + // fn bunStringDealloc(this: *anyopaque, str: *anyopaque, size: u32) callconv(.C) void {} + + /// If all initialization allocations succeed, the arena will be copied + /// into the interpreter struct, so it is not a stale reference and safe to call `arena.deinit()` on error. + pub fn init( + event_loop: JSC.EventLoopHandle, + allocator: Allocator, + arena: *bun.ArenaAllocator, + script: *ast.Script, + jsobjs: []JSValue, + ) shell.Result(*ThisInterpreter) { + var interpreter = allocator.create(ThisInterpreter) catch bun.outOfMemory(); + interpreter.event_loop = event_loop; + interpreter.allocator = allocator; + + const export_env = brk: { + // This will be set in the shell builtin to `process.env` + if (event_loop == .js) break :brk EnvMap.init(allocator); - var iter = env_loader.map.iterator(); - while (iter.next()) |entry| { - const value = EnvStr.initSlice(entry.value_ptr.value); - const key = EnvStr.initSlice(entry.key_ptr.*); - export_env.insert(key, value); + var env_loader: *bun.DotEnv.Loader = env_loader: { + if (event_loop == .js) { + break :env_loader event_loop.js.virtual_machine.bundler.env; } - break :brk export_env; + break :env_loader event_loop.env(); }; - var pathbuf: [bun.MAX_PATH_BYTES]u8 = undefined; - const cwd = switch (Syscall.getcwd(&pathbuf)) { - .result => |cwd| cwd.ptr[0..cwd.len :0], - .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; - }, - }; + // This will save ~2x memory + var export_env = EnvMap.initWithCapacity(allocator, env_loader.map.map.unmanaged.entries.len); - // export_env.put("PWD", cwd) catch bun.outOfMemory(); - // export_env.put("OLDPWD", "/") catch bun.outOfMemory(); + var iter = env_loader.iterator(); - const cwd_fd = switch (Syscall.open(cwd, std.os.O.DIRECTORY | std.os.O.RDONLY, 0)) { - .result => |fd| fd, - .err => |err| { - return .{ .err = .{ .sys = err.toSystemError() } }; - }, - }; - var cwd_arr = std.ArrayList(u8).initCapacity(bun.default_allocator, cwd.len + 1) catch bun.outOfMemory(); - cwd_arr.appendSlice(cwd[0 .. cwd.len + 1]) catch bun.outOfMemory(); - - if (comptime bun.Environment.allow_assert) { - std.debug.assert(cwd_arr.items[cwd_arr.items.len -| 1] == 0); + while (iter.next()) |entry| { + const value = EnvStr.initSlice(entry.value_ptr.value); + const key = EnvStr.initSlice(entry.key_ptr.*); + export_env.insert(key, value); } - interpreter.* = .{ - .global = global, + break :brk export_env; + }; - .script = script, - .allocator = allocator, - .jsobjs = jsobjs, + var pathbuf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd = switch (Syscall.getcwd(&pathbuf)) { + .result => |cwd| cwd.ptr[0..cwd.len :0], + .err => |err| { + return .{ .err = .{ .sys = err.toSystemError() } }; + }, + }; - .arena = arena.*, + // export_env.put("PWD", cwd) catch bun.outOfMemory(); + // export_env.put("OLDPWD", "/") catch bun.outOfMemory(); - .root_shell = ShellState{ - .io = .{}, + const cwd_fd = switch (Syscall.open(cwd, std.os.O.DIRECTORY | std.os.O.RDONLY, 0)) { + .result => |fd| fd, + .err => |err| { + return .{ .err = .{ .sys = err.toSystemError() } }; + }, + }; + var cwd_arr = std.ArrayList(u8).initCapacity(bun.default_allocator, cwd.len + 1) catch bun.outOfMemory(); + cwd_arr.appendSlice(cwd[0 .. cwd.len + 1]) catch bun.outOfMemory(); - .shell_env = EnvMap.init(allocator), - .cmd_local_env = EnvMap.init(allocator), - .export_env = export_env, + if (comptime bun.Environment.allow_assert) { + std.debug.assert(cwd_arr.items[cwd_arr.items.len -| 1] == 0); + } - .__cwd = cwd_arr, - .__prev_cwd = cwd_arr.clone() catch bun.outOfMemory(), - .cwd_fd = cwd_fd, - }, - }; + log("Duping stdin", .{}); + const stdin_fd = switch (ShellSyscall.dup(shell.STDIN_FD)) { + .result => |fd| fd, + .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + }; - if (comptime EventLoopKind == .js) { - interpreter.root_shell.io.stdout = .{ .std = .{ .captured = &interpreter.root_shell._buffered_stdout.owned } }; - interpreter.root_shell.io.stderr = .{ .std = .{ .captured = &interpreter.root_shell._buffered_stderr.owned } }; - } + log("Duping stdout", .{}); + const stdout_fd = switch (ShellSyscall.dup(shell.STDOUT_FD)) { + .result => |fd| fd, + .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + }; - return .{ .result = interpreter }; - } + log("Duping stderr", .{}); + const stderr_fd = switch (ShellSyscall.dup(shell.STDERR_FD)) { + .result => |fd| fd, + .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + }; - pub fn initAndRunFromFile(mini: *JSC.MiniEventLoop, path: []const u8) !void { - var arena = bun.ArenaAllocator.init(bun.default_allocator); - const src = src: { - var file = try std.fs.cwd().openFile(path, .{}); - defer file.close(); - break :src try file.reader().readAllAlloc(arena.allocator(), std.math.maxInt(u32)); - }; - defer arena.deinit(); + const stdin_reader = IOReader.init(stdin_fd, event_loop); + const stdout_writer = IOWriter.init(stdout_fd, event_loop); + const stderr_writer = IOWriter.init(stderr_fd, event_loop); - 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, - &[_]bun.String{}, - &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 {s} due to error {s}", .{ std.fs.path.basename(path), str }); - bun.Global.exit(1); - } - - if (out_parser) |*p| { - const errstr = p.combineErrors(); - bun.Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(path), 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, + interpreter.* = .{ + .event_loop = event_loop, - 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)); - } + .script = script, + .allocator = allocator, + .jsobjs = jsobjs, - 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(); + .arena = arena.*, - 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, &[_]bun.String{}, &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); - } + .root_shell = ShellState{ + .shell_env = EnvMap.init(allocator), + .cmd_local_env = EnvMap.init(allocator), + .export_env = export_env, - 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); - } + .__cwd = cwd_arr, + .__prev_cwd = cwd_arr.clone() catch bun.outOfMemory(), + .cwd_fd = cwd_fd, + }, - 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; + .root_io = .{ + .stdin = .{ + .fd = stdin_reader, }, - .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)); - } + .stdout = .{ + .fd = .{ + .writer = stdout_writer, + }, + }, + .stderr = .{ + .fd = .{ + .writer = stderr_writer, + }, + }, + }, + }; - 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); - root.start(); + if (event_loop == .js) { + interpreter.root_io.stdout.fd.captured = &interpreter.root_shell._buffered_stdout.owned; + interpreter.root_io.stderr.fd.captured = &interpreter.root_shell._buffered_stderr.owned; } - pub fn runFromJS(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { - _ = callframe; // autofix + return .{ .result = interpreter }; + } - _ = globalThis; - incrPendingActivityFlag(&this.has_pending_activity); - var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_shell.io); - this.started.store(true, .SeqCst); - root.start(); - return .undefined; - } + pub fn initAndRunFromFile(mini: *JSC.MiniEventLoop, path: []const u8) !void { + var arena = bun.ArenaAllocator.init(bun.default_allocator); + const src = src: { + var file = try std.fs.cwd().openFile(path, .{}); + 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; + var out_lex_result: ?bun.shell.LexResult = null; + const script = ThisInterpreter.parse( + &arena, + src, + jsobjs, + &[_]bun.String{}, + &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 {s} due to error {s}", .{ std.fs.path.basename(path), str }); + bun.Global.exit(1); + } + + if (out_parser) |*p| { + const errstr = p.combineErrors(); + bun.Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(path), 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 = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { + .err => |*e| { + throwShellErr(e, .{ .mini = mini }); + return; + }, + .result => |i| i, + }; + const IsDone = struct { + done: bool = false, - fn ioToJSValue(this: *ThisInterpreter, buf: *bun.ByteList) JSValue { - const bytelist = buf.*; - buf.* = .{}; - const value = JSC.MarkedArrayBuffer.toNodeBuffer( - .{ - .allocator = bun.default_allocator, - .buffer = JSC.ArrayBuffer.fromBytes(@constCast(bytelist.slice()), .Uint8Array), - }, - this.global, - ); + 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)); + } - return value; - } + 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(); - fn childDone(this: *ThisInterpreter, child: InterpreterChildPtr, exit_code: ExitCode) void { - if (child.ptr.is(Script)) { - const script = child.as(Script); - script.deinitFromInterpreter(); - this.finish(exit_code); - return; + 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, &[_]bun.String{}, &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); } - @panic("Bad child"); - } - fn finish(this: *ThisInterpreter, exit_code: ExitCode) void { - log("finish", .{}); - defer decrPendingActivityFlag(&this.has_pending_activity); - if (comptime EventLoopKind == .js) { - // defer this.deinit(); - // this.promise.resolve(this.global, JSValue.jsNumberFromInt32(@intCast(exit_code))); - // this.buffered_stdout. - this.reject.deinit(); - _ = this.resolve.call(&[_]JSValue{if (comptime bun.Environment.isWindows) JSValue.jsNumberFromU16(exit_code) else JSValue.jsNumberFromChar(exit_code)}); - } else { - this.done.?.* = true; + 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); } - } - fn errored(this: *ThisInterpreter, the_error: ShellError) void { - _ = the_error; // autofix - defer decrPendingActivityFlag(&this.has_pending_activity); + return err; + }; + const script_heap = try arena.allocator().create(ast.Script); + script_heap.* = script; + var interp = switch (ThisInterpreter.init(.{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) { + .err => |*e| { + throwShellErr(e, .{ .mini = mini }); + return; + }, + .result => |i| i, + }; + const IsDone = struct { + done: bool = false, - if (comptime EventLoopKind == .js) { - // defer this.deinit(); - // this.promise.reject(this.global, the_error.toJSC(this.global)); - this.resolve.deinit(); - _ = this.reject.call(&[_]JSValue{JSValue.jsNumberFromChar(1)}); + 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)); + interp.deinit(); + } + + pub fn run(this: *ThisInterpreter) !void { + var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_io.copy()); + this.started.store(true, .SeqCst); + root.start(); + } + + pub fn runFromJS(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue { + _ = callframe; // autofix + + _ = globalThis; + incrPendingActivityFlag(&this.has_pending_activity); + var root = Script.init(this, &this.root_shell, this.script, Script.ParentPtr.init(this), this.root_io.copy()); + this.started.store(true, .SeqCst); + root.start(); + return .undefined; + } + + fn ioToJSValue(this: *ThisInterpreter, buf: *bun.ByteList) JSValue { + const bytelist = buf.*; + buf.* = .{}; + const value = JSC.MarkedArrayBuffer.toNodeBuffer( + .{ + .allocator = bun.default_allocator, + .buffer = JSC.ArrayBuffer.fromBytes(@constCast(bytelist.slice()), .Uint8Array), + }, + this.event_loop.js.global, + ); + + return value; + } + + fn childDone(this: *ThisInterpreter, child: InterpreterChildPtr, exit_code: ExitCode) void { + if (child.ptr.is(Script)) { + const script = child.as(Script); + script.deinitFromInterpreter(); + this.finish(exit_code); + return; } + @panic("Bad child"); + } - fn deinit(this: *ThisInterpreter) void { - log("deinit", .{}); - for (this.jsobjs) |jsobj| { - jsobj.unprotect(); - } - this.resolve.deinit(); + fn finish(this: *ThisInterpreter, exit_code: ExitCode) void { + log("finish", .{}); + defer decrPendingActivityFlag(&this.has_pending_activity); + if (this.event_loop == .js) { + // defer this.deinit(); + // this.promise.resolve(this.global, JSValue.jsNumberFromInt32(@intCast(exit_code))); + // this.buffered_stdout. this.reject.deinit(); - this.root_shell.deinitImpl(false, true); - this.allocator.destroy(this); + _ = this.resolve.call(&.{JSValue.jsNumberFromU16(exit_code)}); + } else { + this.done.?.* = true; } + } - pub fn setResolve(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const value = callframe.argument(0); - if (!value.isCallable(globalThis.vm())) { - globalThis.throwInvalidArguments("resolve must be a function", .{}); - return .undefined; - } - this.resolve.set(globalThis, value.withAsyncContextIfNeeded(globalThis)); - return .undefined; + fn errored(this: *ThisInterpreter, the_error: ShellError) void { + _ = the_error; // autofix + defer decrPendingActivityFlag(&this.has_pending_activity); + + if (this.event_loop == .js) { + // defer this.deinit(); + // this.promise.reject(this.global, the_error.toJSC(this.global)); + this.resolve.deinit(); + _ = this.reject.call(&[_]JSValue{JSValue.jsNumberFromChar(1)}); } + } - pub fn setReject(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const value = callframe.argument(0); - if (!value.isCallable(globalThis.vm())) { - globalThis.throwInvalidArguments("reject must be a function", .{}); - return .undefined; - } - this.reject.set(globalThis, value.withAsyncContextIfNeeded(globalThis)); - return .undefined; + fn deinit(this: *ThisInterpreter) void { + log("deinit interpreter", .{}); + for (this.jsobjs) |jsobj| { + jsobj.unprotect(); } + this.root_io.deref(); + this.resolve.deinit(); + this.reject.deinit(); + this.root_shell.deinitImpl(false, true); + this.allocator.destroy(this); + } - pub fn setQuiet(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - _ = globalThis; - _ = callframe; - this.root_shell.io.stdout = .pipe; - this.root_shell.io.stderr = .pipe; + pub fn setResolve(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const value = callframe.argument(0); + if (!value.isCallable(globalThis.vm())) { + globalThis.throwInvalidArguments("resolve must be a function", .{}); return .undefined; } + this.resolve.set(globalThis, value.withAsyncContextIfNeeded(globalThis)); + return .undefined; + } - pub fn setCwd(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const value = callframe.argument(0); - const str = bun.String.fromJS(value, globalThis); - - const slice = str.toUTF8(bun.default_allocator); - defer slice.deinit(); - switch (this.root_shell.changeCwd(this, slice.slice())) { - .err => |e| { - globalThis.throwValue(e.toJSC(globalThis)); - return .undefined; - }, - .result => {}, - } + pub fn setReject(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const value = callframe.argument(0); + if (!value.isCallable(globalThis.vm())) { + globalThis.throwInvalidArguments("reject must be a function", .{}); return .undefined; } + this.reject.set(globalThis, value.withAsyncContextIfNeeded(globalThis)); + return .undefined; + } + + pub fn setQuiet(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + log("Interpreter(0x{x}) setQuiet()", .{@intFromPtr(this)}); + _ = globalThis; + _ = callframe; + this.root_io.stdout.deref(); + this.root_io.stderr.deref(); + this.root_io.stdout = .pipe; + this.root_io.stderr = .pipe; + return .undefined; + } - pub fn setEnv(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { - const value1 = callframe.argument(0); - if (!value1.isObject()) { - globalThis.throwInvalidArguments("env must be an object", .{}); + pub fn setCwd(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const value = callframe.argument(0); + const str = bun.String.fromJS(value, globalThis); + + const slice = str.toUTF8(bun.default_allocator); + defer slice.deinit(); + switch (this.root_shell.changeCwd(this, slice.slice())) { + .err => |e| { + globalThis.throwValue(e.toJSC(globalThis)); return .undefined; - } + }, + .result => {}, + } + return .undefined; + } - var object_iter = JSC.JSPropertyIterator(.{ - .skip_empty_name = false, - .include_value = true, - }).init(globalThis, value1.asObjectRef()); - defer object_iter.deinit(); + pub fn setEnv(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const value1 = callframe.argument(0); + if (!value1.isObject()) { + globalThis.throwInvalidArguments("env must be an object", .{}); + return .undefined; + } - this.root_shell.export_env.clearRetainingCapacity(); - this.root_shell.export_env.ensureTotalCapacity(object_iter.len); + var object_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(globalThis, value1.asObjectRef()); + defer object_iter.deinit(); - // If the env object does not include a $PATH, it must disable path lookup for argv[0] - // PATH = ""; + this.root_shell.export_env.clearRetainingCapacity(); + this.root_shell.export_env.ensureTotalCapacity(object_iter.len); - while (object_iter.next()) |key| { - const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); - var value = object_iter.value; - if (value == .undefined) continue; + // If the env object does not include a $PATH, it must disable path lookup for argv[0] + // PATH = ""; - const value_str = value.getZigString(globalThis); - const slice = value_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); - const keyref = EnvStr.initRefCounted(keyslice); - defer keyref.deref(); - const valueref = EnvStr.initRefCounted(slice); - defer valueref.deref(); + while (object_iter.next()) |key| { + const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); + var value = object_iter.value; + if (value == .undefined) continue; - this.root_shell.export_env.insert(keyref, valueref); - } + const value_str = value.getZigString(globalThis); + const slice = value_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); + const keyref = EnvStr.initRefCounted(keyslice); + defer keyref.deref(); + const valueref = EnvStr.initRefCounted(slice); + defer valueref.deref(); - return .undefined; + this.root_shell.export_env.insert(keyref, valueref); } - pub fn isRunning( - this: *ThisInterpreter, - globalThis: *JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - _ = globalThis; // autofix - _ = callframe; // autofix + return .undefined; + } - return JSC.JSValue.jsBoolean(this.hasPendingActivity()); - } + pub fn isRunning( + this: *ThisInterpreter, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + _ = globalThis; // autofix + _ = callframe; // autofix - pub fn getStarted( - this: *ThisInterpreter, - globalThis: *JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - _ = globalThis; // autofix - _ = callframe; // autofix + return JSC.JSValue.jsBoolean(this.hasPendingActivity()); + } - return JSC.JSValue.jsBoolean(this.started.load(.SeqCst)); - } + pub fn getStarted( + this: *ThisInterpreter, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + _ = globalThis; // autofix + _ = callframe; // autofix - pub fn getBufferedStdout( - this: *ThisInterpreter, - globalThis: *JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - _ = globalThis; // autofix - _ = callframe; // autofix + return JSC.JSValue.jsBoolean(this.started.load(.SeqCst)); + } - const stdout = this.ioToJSValue(this.root_shell.buffered_stdout()); - return stdout; - } + pub fn getBufferedStdout( + this: *ThisInterpreter, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + _ = globalThis; // autofix + _ = callframe; // autofix - pub fn getBufferedStderr( - this: *ThisInterpreter, - globalThis: *JSGlobalObject, - callframe: *JSC.CallFrame, - ) callconv(.C) JSC.JSValue { - _ = globalThis; // autofix - _ = callframe; // autofix + const stdout = this.ioToJSValue(this.root_shell.buffered_stdout()); + return stdout; + } - const stdout = this.ioToJSValue(this.root_shell.buffered_stderr()); - return stdout; - } + pub fn getBufferedStderr( + this: *ThisInterpreter, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSC.JSValue { + _ = globalThis; // autofix + _ = callframe; // autofix - pub fn finalize( - this: *ThisInterpreter, - ) callconv(.C) void { - log("Interpreter finalize", .{}); - this.deinit(); - } + const stdout = this.ioToJSValue(this.root_shell.buffered_stderr()); + return stdout; + } - pub fn hasPendingActivity(this: *ThisInterpreter) callconv(.C) bool { - @fence(.SeqCst); - return this.has_pending_activity.load(.SeqCst) > 0; - } + pub fn finalize( + this: *ThisInterpreter, + ) callconv(.C) void { + log("Interpreter finalize", .{}); + this.deinit(); + } - fn incrPendingActivityFlag(has_pending_activity: *std.atomic.Value(usize)) void { - @fence(.SeqCst); - _ = has_pending_activity.fetchAdd(1, .SeqCst); - } + pub fn hasPendingActivity(this: *ThisInterpreter) callconv(.C) bool { + @fence(.SeqCst); + return this.has_pending_activity.load(.SeqCst) > 0; + } - fn decrPendingActivityFlag(has_pending_activity: *std.atomic.Value(usize)) void { - @fence(.SeqCst); - _ = has_pending_activity.fetchSub(1, .SeqCst); - } + fn incrPendingActivityFlag(has_pending_activity: *std.atomic.Value(usize)) void { + @fence(.SeqCst); + _ = has_pending_activity.fetchAdd(1, .SeqCst); + log("Interpreter incr pending activity {d}", .{has_pending_activity.load(.SeqCst)}); + } - const AssignCtx = enum { - cmd, - shell, - exported, - }; + fn decrPendingActivityFlag(has_pending_activity: *std.atomic.Value(usize)) void { + @fence(.SeqCst); + _ = has_pending_activity.fetchSub(1, .SeqCst); + log("Interpreter decr pending activity {d}", .{has_pending_activity.load(.SeqCst)}); + } - const ExpansionOpts = struct { - for_spawn: bool = true, - single: bool = false, - }; + pub fn rootIO(this: *const Interpreter) *const IO { + return &this.root_io; + } - /// TODO PERF: in the case of expanding cmd args, we probably want to use the spawn args arena - /// otherwise the interpreter allocator - /// - /// If a word contains command substitution or glob expansion syntax then it - /// needs to do IO, so we have to keep track of the state for that. - pub const Expansion = struct { - base: State, - node: *const ast.Atom, - parent: ParentPtr, + const AssignCtx = enum { + cmd, + shell, + exported, + }; - word_idx: u32, - current_out: std.ArrayList(u8), - state: union(enum) { - normal, - braces, - glob, - done, - err: bun.shell.ShellErr, + const ExpansionOpts = struct { + for_spawn: bool = true, + single: bool = false, + }; + + /// TODO PERF: in the case of expanding cmd args, we probably want to use the spawn args arena + /// otherwise the interpreter allocator + /// + /// If a word contains command substitution or glob expansion syntax then it + /// needs to do IO, so we have to keep track of the state for that. + pub const Expansion = struct { + base: State, + node: *const ast.Atom, + parent: ParentPtr, + io: IO, + + word_idx: u32, + current_out: std.ArrayList(u8), + state: union(enum) { + normal, + braces, + glob, + done, + err: bun.shell.ShellErr, + }, + child_state: union(enum) { + idle, + cmd_subst: struct { + cmd: *Script, + quoted: bool = false, }, - child_state: union(enum) { - idle, - cmd_subst: struct { - cmd: *Script, - quoted: bool = false, - }, - // TODO - glob: struct { - initialized: bool = false, - walker: GlobWalker, - }, + // TODO + glob: struct { + initialized: bool = false, + walker: GlobWalker, }, - out: Result, - out_idx: u32, - - const ParentPtr = StatePtrUnion(.{ - Cmd, - Assigns, - }); + }, + out: Result, + out_idx: u32, - const ChildPtr = StatePtrUnion(.{ - // Cmd, - Script, - }); + const ParentPtr = StatePtrUnion(.{ + Cmd, + Assigns, + }); - const Result = union(enum) { - array_of_slice: *std.ArrayList([:0]const u8), - array_of_ptr: *std.ArrayList(?[*:0]const u8), - single: struct { - list: *std.ArrayList(u8), - done: bool = false, - }, + const ChildPtr = StatePtrUnion(.{ + // Cmd, + Script, + }); - pub fn pushResultSlice(this: *Result, buf: [:0]const u8) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(buf[buf.len] == 0); - } + const Result = union(enum) { + array_of_slice: *std.ArrayList([:0]const u8), + array_of_ptr: *std.ArrayList(?[*:0]const u8), + single: struct { + list: *std.ArrayList(u8), + done: bool = false, + }, - switch (this.*) { - .array_of_slice => { - this.array_of_slice.append(buf) catch bun.outOfMemory(); - }, - .array_of_ptr => { - this.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.ptr))) catch bun.outOfMemory(); - }, - .single => { - if (this.single.done) return; - this.single.list.appendSlice(buf[0 .. buf.len + 1]) catch bun.outOfMemory(); - this.single.done = true; - }, - } + pub fn pushResultSlice(this: *Result, buf: [:0]const u8) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(buf[buf.len] == 0); } - pub fn pushResult(this: *Result, buf: *std.ArrayList(u8)) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(buf.items[buf.items.len - 1] == 0); - } + switch (this.*) { + .array_of_slice => { + this.array_of_slice.append(buf) catch bun.outOfMemory(); + }, + .array_of_ptr => { + this.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.ptr))) catch bun.outOfMemory(); + }, + .single => { + if (this.single.done) return; + this.single.list.appendSlice(buf[0 .. buf.len + 1]) catch bun.outOfMemory(); + this.single.done = true; + }, + } + } - switch (this.*) { - .array_of_slice => { - this.array_of_slice.append(buf.items[0 .. buf.items.len - 1 :0]) catch bun.outOfMemory(); - }, - .array_of_ptr => { - this.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.items.ptr))) catch bun.outOfMemory(); - }, - .single => { - if (this.single.done) return; - this.single.list.appendSlice(buf.items[0..]) catch bun.outOfMemory(); - }, - } + pub fn pushResult(this: *Result, buf: *std.ArrayList(u8)) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(buf.items[buf.items.len - 1] == 0); } - }; - pub fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - expansion: *Expansion, - node: *const ast.Atom, - parent: ParentPtr, - out_result: Result, - ) void { - expansion.* = .{ - .node = node, - .base = .{ - .kind = .expansion, - .interpreter = interpreter, - .shell = shell_state, + switch (this.*) { + .array_of_slice => { + this.array_of_slice.append(buf.items[0 .. buf.items.len - 1 :0]) catch bun.outOfMemory(); }, - .parent = parent, - - .word_idx = 0, - .state = .normal, - .child_state = .idle, - .out = out_result, - .out_idx = 0, - .current_out = std.ArrayList(u8).init(interpreter.allocator), - }; - // var expansion = interpreter.allocator.create(Expansion) catch bun.outOfMemory(); + .array_of_ptr => { + this.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.items.ptr))) catch bun.outOfMemory(); + }, + .single => { + if (this.single.done) return; + this.single.list.appendSlice(buf.items[0..]) catch bun.outOfMemory(); + }, + } } + }; - pub fn deinit(expansion: *Expansion) void { - expansion.current_out.deinit(); - } + pub fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, + expansion: *Expansion, + node: *const ast.Atom, + parent: ParentPtr, + out_result: Result, + io: IO, + ) void { + log("Expansion(0x{x}) init", .{@intFromPtr(expansion)}); + expansion.* = .{ + .node = node, + .base = .{ + .kind = .expansion, + .interpreter = interpreter, + .shell = shell_state, + }, + .parent = parent, + + .word_idx = 0, + .state = .normal, + .child_state = .idle, + .out = out_result, + .out_idx = 0, + .current_out = std.ArrayList(u8).init(interpreter.allocator), + .io = io, + }; + // var expansion = interpreter.allocator.create(Expansion) catch bun.outOfMemory(); + } - pub fn start(this: *Expansion) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.child_state == .idle); - std.debug.assert(this.word_idx == 0); - } + pub fn deinit(expansion: *Expansion) void { + log("Expansion(0x{x}) deinit", .{@intFromPtr(expansion)}); + expansion.current_out.deinit(); + expansion.io.deinit(); + } - this.state = .normal; - this.next(); + pub fn start(this: *Expansion) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.child_state == .idle); + std.debug.assert(this.word_idx == 0); } - pub fn next(this: *Expansion) void { - while (!(this.state == .done or this.state == .err)) { - switch (this.state) { - .normal => { - // initialize - if (this.word_idx == 0) { - var has_unknown = false; - // + 1 for sentinel - const string_size = this.expansionSizeHint(this.node, &has_unknown); - this.current_out.ensureUnusedCapacity(string_size + 1) catch bun.outOfMemory(); - } + this.state = .normal; + this.next(); + } - while (this.word_idx < this.node.atomsLen()) { - const is_cmd_subst = this.expandVarAndCmdSubst(this.word_idx); - // yield execution - if (is_cmd_subst) return; - } + pub fn next(this: *Expansion) void { + while (!(this.state == .done or this.state == .err)) { + switch (this.state) { + .normal => { + // initialize + if (this.word_idx == 0) { + var has_unknown = false; + // + 1 for sentinel + const string_size = this.expansionSizeHint(this.node, &has_unknown); + this.current_out.ensureUnusedCapacity(string_size + 1) catch bun.outOfMemory(); + } - if (this.word_idx >= this.node.atomsLen()) { - // NOTE brace expansion + cmd subst has weird behaviour we don't support yet, ex: - // echo $(echo a b c){1,2,3} - // >> a b c1 a b c2 a b c3 - if (this.node.has_brace_expansion()) { - this.state = .braces; - continue; - } + while (this.word_idx < this.node.atomsLen()) { + const is_cmd_subst = this.expandVarAndCmdSubst(this.word_idx); + // yield execution + if (is_cmd_subst) return; + } - if (this.node.has_glob_expansion()) { - this.state = .glob; - continue; - } + if (this.word_idx >= this.node.atomsLen()) { + // NOTE brace expansion + cmd subst has weird behaviour we don't support yet, ex: + // echo $(echo a b c){1,2,3} + // >> a b c1 a b c2 a b c3 + if (this.node.has_brace_expansion()) { + this.state = .braces; + continue; + } - this.pushCurrentOut(); - this.state = .done; + if (this.node.has_glob_expansion()) { + this.state = .glob; continue; } - // Shouldn't fall through to here - std.debug.assert(this.word_idx >= this.node.atomsLen()); - return; - }, - .braces => { - var arena = Arena.init(this.base.interpreter.allocator); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - const brace_str = this.current_out.items[0..]; - // FIXME some of these errors aren't alloc errors for example lexer parser errors - var lexer_output = Braces.Lexer.tokenize(arena_allocator, brace_str) catch |e| OOM(e); - const expansion_count = Braces.calculateExpandedAmount(lexer_output.tokens.items[0..]) catch |e| OOM(e); - - var expanded_strings = brk: { - const stack_max = comptime 16; - comptime { - std.debug.assert(@sizeOf([]std.ArrayList(u8)) * stack_max <= 256); - } - var maybe_stack_alloc = std.heap.stackFallback(@sizeOf([]std.ArrayList(u8)) * stack_max, this.base.interpreter.allocator); - const expanded_strings = maybe_stack_alloc.get().alloc(std.ArrayList(u8), expansion_count) catch bun.outOfMemory(); - break :brk expanded_strings; - }; + this.pushCurrentOut(); + this.state = .done; + continue; + } - for (0..expansion_count) |i| { - expanded_strings[i] = std.ArrayList(u8).init(this.base.interpreter.allocator); + // Shouldn't fall through to here + std.debug.assert(this.word_idx >= this.node.atomsLen()); + return; + }, + .braces => { + var arena = Arena.init(this.base.interpreter.allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + const brace_str = this.current_out.items[0..]; + // FIXME some of these errors aren't alloc errors for example lexer parser errors + var lexer_output = Braces.Lexer.tokenize(arena_allocator, brace_str) catch |e| OOM(e); + const expansion_count = Braces.calculateExpandedAmount(lexer_output.tokens.items[0..]) catch |e| OOM(e); + + var expanded_strings = brk: { + const stack_max = comptime 16; + comptime { + std.debug.assert(@sizeOf([]std.ArrayList(u8)) * stack_max <= 256); } + var maybe_stack_alloc = std.heap.stackFallback(@sizeOf([]std.ArrayList(u8)) * stack_max, this.base.interpreter.allocator); + const expanded_strings = maybe_stack_alloc.get().alloc(std.ArrayList(u8), expansion_count) catch bun.outOfMemory(); + break :brk expanded_strings; + }; - Braces.expand( - arena_allocator, - lexer_output.tokens.items[0..], - expanded_strings, - lexer_output.contains_nested, - ) catch bun.outOfMemory(); + for (0..expansion_count) |i| { + expanded_strings[i] = std.ArrayList(u8).init(this.base.interpreter.allocator); + } - this.outEnsureUnusedCapacity(expansion_count); + Braces.expand( + arena_allocator, + lexer_output.tokens.items[0..], + expanded_strings, + lexer_output.contains_nested, + ) catch bun.outOfMemory(); - // Add sentinel values - for (0..expansion_count) |i| { - expanded_strings[i].append(0) catch bun.outOfMemory(); - this.pushResult(&expanded_strings[i]); - } + this.outEnsureUnusedCapacity(expansion_count); - if (this.node.has_glob_expansion()) { - this.state = .glob; - } else { - this.state = .done; - } - }, - .glob => { - this.transitionToGlobState(); - // yield - return; - }, - .done, .err => unreachable, - } - } + // Add sentinel values + for (0..expansion_count) |i| { + expanded_strings[i].append(0) catch bun.outOfMemory(); + this.pushResult(&expanded_strings[i]); + } - if (this.state == .done) { - this.parent.childDone(this, 0); - return; + if (this.node.has_glob_expansion()) { + this.state = .glob; + } else { + this.state = .done; + } + }, + .glob => { + this.transitionToGlobState(); + // yield + return; + }, + .done, .err => unreachable, } + } - // Parent will inspect the `this.state.err` - if (this.state == .err) { - this.parent.childDone(this, 1); - return; - } + if (this.state == .done) { + this.parent.childDone(this, 0); + return; } - fn transitionToGlobState(this: *Expansion) void { - var arena = Arena.init(this.base.interpreter.allocator); - this.child_state = .{ .glob = .{ .walker = .{} } }; - const pattern = this.current_out.items[0..]; + // Parent will inspect the `this.state.err` + if (this.state == .err) { + this.parent.childDone(this, 1); + return; + } + } - const cwd = this.base.shell.cwd(); + fn transitionToGlobState(this: *Expansion) void { + var arena = Arena.init(this.base.interpreter.allocator); + this.child_state = .{ .glob = .{ .walker = .{} } }; + const pattern = this.current_out.items[0..]; - switch (GlobWalker.initWithCwd( - &this.child_state.glob.walker, - &arena, - pattern, - cwd, - false, - false, - false, - false, - false, - ) catch bun.outOfMemory()) { - .result => {}, - .err => |e| { - this.state = .{ .err = bun.shell.ShellErr.newSys(e) }; - this.next(); - return; - }, - } + const cwd = this.base.shell.cwd(); - var task = ShellGlobTask.createOnMainThread(this.base.interpreter.allocator, &this.child_state.glob.walker, this); - task.schedule(); + switch (GlobWalker.initWithCwd( + &this.child_state.glob.walker, + &arena, + pattern, + cwd, + false, + false, + false, + false, + false, + ) catch bun.outOfMemory()) { + .result => {}, + .err => |e| { + this.state = .{ .err = bun.shell.ShellErr.newSys(e) }; + this.next(); + return; + }, } - pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { - switch (this.node.*) { - .simple => |*simp| { - const is_cmd_subst = this.expandSimpleNoIO(simp, &this.current_out); + var task = ShellGlobTask.createOnMainThread(this.base.interpreter.allocator, &this.child_state.glob.walker, this); + task.schedule(); + } + + pub fn expandVarAndCmdSubst(this: *Expansion, start_word_idx: u32) bool { + switch (this.node.*) { + .simple => |*simp| { + const is_cmd_subst = this.expandSimpleNoIO(simp, &this.current_out); + if (is_cmd_subst) { + const io: IO = .{ + .stdin = this.base.rootIO().stdin.ref(), + .stdout = .pipe, + .stderr = this.base.rootIO().stderr.ref(), + }; + const shell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, io, .cmd_subst)) { + .result => |s| s, + .err => |e| { + this.base.throw(&bun.shell.ShellErr.newSys(e)); + return false; + }, + }; + var script = Script.init(this.base.interpreter, shell_state, &this.node.simple.cmd_subst.script, Script.ParentPtr.init(this), io); + this.child_state = .{ + .cmd_subst = .{ + .cmd = script, + .quoted = simp.cmd_subst.quoted, + }, + }; + script.start(); + return true; + } else { + this.word_idx += 1; + } + }, + .compound => |cmp| { + for (cmp.atoms[start_word_idx..]) |*simple_atom| { + const is_cmd_subst = this.expandSimpleNoIO(simple_atom, &this.current_out); if (is_cmd_subst) { - var io: IO = .{}; - io.stdout = .pipe; - io.stderr = this.base.shell.io.stderr; + const io: IO = .{ + .stdin = this.base.rootIO().stdin.ref(), + .stdout = .pipe, + .stderr = this.base.rootIO().stderr.ref(), + }; const shell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, io, .cmd_subst)) { .result => |s| s, .err => |e| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(e)); + this.base.throw(&bun.shell.ShellErr.newSys(e)); return false; }, }; - var script = Script.init(this.base.interpreter, shell_state, &this.node.simple.cmd_subst.script, Script.ParentPtr.init(this), io); + var script = Script.init(this.base.interpreter, shell_state, &simple_atom.cmd_subst.script, Script.ParentPtr.init(this), io); this.child_state = .{ .cmd_subst = .{ .cmd = script, - .quoted = simp.cmd_subst.quoted, + .quoted = simple_atom.cmd_subst.quoted, }, }; script.start(); return true; } else { this.word_idx += 1; + this.child_state = .idle; } - }, - .compound => |cmp| { - for (cmp.atoms[start_word_idx..]) |*simple_atom| { - const is_cmd_subst = this.expandSimpleNoIO(simple_atom, &this.current_out); - if (is_cmd_subst) { - var io: IO = .{}; - io.stdout = .pipe; - io.stderr = this.base.shell.io.stderr; - const shell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, io, .cmd_subst)) { - .result => |s| s, - .err => |e| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(e)); - return false; - }, - }; - var script = Script.init(this.base.interpreter, shell_state, &simple_atom.cmd_subst.script, Script.ParentPtr.init(this), io); - this.child_state = .{ - .cmd_subst = .{ - .cmd = script, - .quoted = simple_atom.cmd_subst.quoted, - }, - }; - script.start(); - return true; - } else { - this.word_idx += 1; - this.child_state = .idle; - } - } - }, - } - - return false; - } - - /// Remove a set of values from the beginning and end of a slice. - pub fn trim(slice: []u8, values_to_strip: []const u8) []u8 { - var begin: usize = 0; - var end: usize = slice.len; - while (begin < end and std.mem.indexOfScalar(u8, values_to_strip, slice[begin]) != null) : (begin += 1) {} - while (end > begin and std.mem.indexOfScalar(u8, values_to_strip, slice[end - 1]) != null) : (end -= 1) {} - return slice[begin..end]; - } - - /// 1. Turn all newlines into spaces - /// 2. Strip last newline if it exists - /// 3. Trim leading, trailing, and consecutive whitespace - fn postSubshellExpansion(this: *Expansion, stdout_: []u8) void { - // 1. and 2. - var stdout = convertNewlinesToSpaces(stdout_); - - // Trim leading & trailing whitespace - stdout = trim(stdout, " \n \r\t"); - if (stdout.len == 0) return; - - // Trim consecutive - var prev_whitespace: bool = false; - var a: usize = 0; - var b: usize = 1; - for (stdout[0..], 0..) |c, i| { - if (prev_whitespace) { - if (c != ' ') { - // this. - a = i; - b = i + 1; - prev_whitespace = false; - } - continue; - } - - b = i + 1; - if (c == ' ') { - b = i; - prev_whitespace = true; - this.current_out.appendSlice(stdout[a..b]) catch bun.outOfMemory(); - this.pushCurrentOut(); - // const slice_z = this.base.interpreter.allocator.dupeZ(u8, stdout[a..b]) catch bun.outOfMemory(); - // this.pushResultSlice(slice_z); } - } - // "aa bbb" - - this.current_out.appendSlice(stdout[a..b]) catch bun.outOfMemory(); - // this.pushCurrentOut(); - // const slice_z = this.base.interpreter.allocator.dupeZ(u8, stdout[a..b]) catch bun.outOfMemory(); - // this.pushResultSlice(slice_z); + }, } - fn convertNewlinesToSpaces(stdout_: []u8) []u8 { - var stdout = brk: { - if (stdout_.len == 0) return stdout_; - if (stdout_[stdout_.len -| 1] == '\n') break :brk stdout_[0..stdout_.len -| 1]; - break :brk stdout_[0..]; - }; + return false; + } - if (stdout.len == 0) { - // out.append('\n') catch bun.outOfMemory(); - return stdout; - } + /// Remove a set of values from the beginning and end of a slice. + pub fn trim(slice: []u8, values_to_strip: []const u8) []u8 { + var begin: usize = 0; + var end: usize = slice.len; + while (begin < end and std.mem.indexOfScalar(u8, values_to_strip, slice[begin]) != null) : (begin += 1) {} + while (end > begin and std.mem.indexOfScalar(u8, values_to_strip, slice[end - 1]) != null) : (end -= 1) {} + return slice[begin..end]; + } - // From benchmarks the SIMD stuff only is faster when chars >= 64 - if (stdout.len < 64) { - convertNewlinesToSpacesSlow(0, stdout); - // out.appendSlice(stdout[0..]) catch bun.outOfMemory(); - return stdout[0..]; - } + /// 1. Turn all newlines into spaces + /// 2. Strip last newline if it exists + /// 3. Trim leading, trailing, and consecutive whitespace + fn postSubshellExpansion(this: *Expansion, stdout_: []u8) void { + // 1. and 2. + var stdout = convertNewlinesToSpaces(stdout_); + + // Trim leading & trailing whitespace + stdout = trim(stdout, " \n \r\t"); + if (stdout.len == 0) return; + + // Trim consecutive + var prev_whitespace: bool = false; + var a: usize = 0; + var b: usize = 1; + for (stdout[0..], 0..) |c, i| { + if (prev_whitespace) { + if (c != ' ') { + // this. + a = i; + b = i + 1; + prev_whitespace = false; + } + continue; + } + + b = i + 1; + if (c == ' ') { + b = i; + prev_whitespace = true; + this.current_out.appendSlice(stdout[a..b]) catch bun.outOfMemory(); + this.pushCurrentOut(); + // const slice_z = this.base.interpreter.allocator.dupeZ(u8, stdout[a..b]) catch bun.outOfMemory(); + // this.pushResultSlice(slice_z); + } + } + // "aa bbb" + + this.current_out.appendSlice(stdout[a..b]) catch bun.outOfMemory(); + // this.pushCurrentOut(); + // const slice_z = this.base.interpreter.allocator.dupeZ(u8, stdout[a..b]) catch bun.outOfMemory(); + // this.pushResultSlice(slice_z); + } - const needles: @Vector(16, u8) = @splat('\n'); - const spaces: @Vector(16, u8) = @splat(' '); - var i: usize = 0; - while (i + 16 <= stdout.len) : (i += 16) { - const haystack: @Vector(16, u8) = stdout[i..][0..16].*; - stdout[i..][0..16].* = @select(u8, haystack == needles, spaces, haystack); - } + fn convertNewlinesToSpaces(stdout_: []u8) []u8 { + var stdout = brk: { + if (stdout_.len == 0) return stdout_; + if (stdout_[stdout_.len -| 1] == '\n') break :brk stdout_[0..stdout_.len -| 1]; + break :brk stdout_[0..]; + }; + + if (stdout.len == 0) { + // out.append('\n') catch bun.outOfMemory(); + return stdout; + } - if (i < stdout.len) convertNewlinesToSpacesSlow(i, stdout); + // From benchmarks the SIMD stuff only is faster when chars >= 64 + if (stdout.len < 64) { + convertNewlinesToSpacesSlow(0, stdout); // out.appendSlice(stdout[0..]) catch bun.outOfMemory(); return stdout[0..]; } - fn convertNewlinesToSpacesSlow(i: usize, stdout: []u8) void { - for (stdout[i..], i..) |c, j| { - if (c == '\n') { - stdout[j] = ' '; - } - } + const needles: @Vector(16, u8) = @splat('\n'); + const spaces: @Vector(16, u8) = @splat(' '); + var i: usize = 0; + while (i + 16 <= stdout.len) : (i += 16) { + const haystack: @Vector(16, u8) = stdout[i..][0..16].*; + stdout[i..][0..16].* = @select(u8, haystack == needles, spaces, haystack); } - fn childDone(this: *Expansion, child: ChildPtr, exit_code: ExitCode) void { - _ = exit_code; - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state != .done and this.state != .err); - std.debug.assert(this.child_state != .idle); - } - - // Command substitution - if (child.ptr.is(Script)) { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.child_state == .cmd_subst); - } - - const stdout = this.child_state.cmd_subst.cmd.base.shell.buffered_stdout().slice(); - if (!this.child_state.cmd_subst.quoted) { - this.postSubshellExpansion(stdout); - } else { - const trimmed = std.mem.trimRight(u8, stdout, " \n\t\r"); - this.current_out.appendSlice(trimmed) catch bun.outOfMemory(); - } + if (i < stdout.len) convertNewlinesToSpacesSlow(i, stdout); + // out.appendSlice(stdout[0..]) catch bun.outOfMemory(); + return stdout[0..]; + } - this.word_idx += 1; - this.child_state = .idle; - child.deinit(); - this.next(); - return; + fn convertNewlinesToSpacesSlow(i: usize, stdout: []u8) void { + for (stdout[i..], i..) |c, j| { + if (c == '\n') { + stdout[j] = ' '; } + } + } - unreachable; + fn childDone(this: *Expansion, child: ChildPtr, exit_code: ExitCode) void { + _ = exit_code; + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state != .done and this.state != .err); + std.debug.assert(this.child_state != .idle); } - fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { + // Command substitution + if (child.ptr.is(Script)) { if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.child_state == .glob); - } - - if (task.err != null) { - switch (task.err.?) { - .syscall => global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(task.err.?.syscall)), - .unknown => |errtag| { - global_handle.get().actuallyThrow(.{ - .custom = bun.default_allocator.dupe(u8, @errorName(errtag)) catch bun.outOfMemory(), - }); - }, - } - } - - if (task.result.items.len == 0) { - const msg = std.fmt.allocPrint(bun.default_allocator, "no matches found: {s}", .{this.child_state.glob.walker.pattern}) catch bun.outOfMemory(); - this.state = .{ - .err = bun.shell.ShellErr{ - .custom = msg, - }, - }; - this.child_state.glob.walker.deinit(true); - this.child_state = .idle; - this.next(); - return; + std.debug.assert(this.child_state == .cmd_subst); } - for (task.result.items) |sentinel_str| { - // The string is allocated in the glob walker arena and will be freed, so needs to be duped here - const duped = this.base.interpreter.allocator.dupeZ(u8, sentinel_str[0..sentinel_str.len]) catch bun.outOfMemory(); - this.pushResultSlice(duped); + const stdout = this.child_state.cmd_subst.cmd.base.shell.buffered_stdout().slice(); + if (!this.child_state.cmd_subst.quoted) { + this.postSubshellExpansion(stdout); + } else { + const trimmed = std.mem.trimRight(u8, stdout, " \n\t\r"); + this.current_out.appendSlice(trimmed) catch bun.outOfMemory(); } this.word_idx += 1; - this.child_state.glob.walker.deinit(true); this.child_state = .idle; - this.state = .done; + child.deinit(); this.next(); + return; } - /// If the atom is actually a command substitution then does nothing and returns true - pub fn expandSimpleNoIO(this: *Expansion, atom: *const ast.SimpleAtom, str_list: *std.ArrayList(u8)) bool { - switch (atom.*) { - .Text => |txt| { - str_list.appendSlice(txt) catch bun.outOfMemory(); - }, - .Var => |label| { - str_list.appendSlice(this.expandVar(label).slice()) catch bun.outOfMemory(); - }, - .asterisk => { - str_list.append('*') catch bun.outOfMemory(); - }, - .double_asterisk => { - str_list.appendSlice("**") catch bun.outOfMemory(); - }, - .brace_begin => { - str_list.append('{') catch bun.outOfMemory(); - }, - .brace_end => { - str_list.append('}') catch bun.outOfMemory(); - }, - .comma => { - str_list.append(',') catch bun.outOfMemory(); - }, - .cmd_subst => { - // TODO: - // if the command substution is comprised of solely shell variable assignments then it should do nothing - // if (atom.cmd_subst.* == .assigns) return false; - return true; - }, - } - return false; - } - - pub fn appendSlice(this: *Expansion, buf: *std.ArrayList(u8), slice: []const u8) void { - _ = this; - buf.appendSlice(slice) catch bun.outOfMemory(); - } - - pub fn pushResultSlice(this: *Expansion, buf: [:0]const u8) void { - this.out.pushResultSlice(buf); - // if (comptime bun.Environment.allow_assert) { - // std.debug.assert(buf.len > 0 and buf[buf.len] == 0); - // } - - // if (this.out == .array_of_slice) { - // this.out.array_of_slice.append(buf) catch bun.outOfMemory(); - // return; - // } - - // this.out.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.ptr))) catch bun.outOfMemory(); - } - - pub fn pushCurrentOut(this: *Expansion) void { - if (this.current_out.items.len == 0) return; - if (this.current_out.items[this.current_out.items.len - 1] != 0) this.current_out.append(0) catch bun.outOfMemory(); - this.pushResult(&this.current_out); - this.current_out = std.ArrayList(u8).init(this.base.interpreter.allocator); - } - - pub fn pushResult(this: *Expansion, buf: *std.ArrayList(u8)) void { - this.out.pushResult(buf); - // if (comptime bun.Environment.allow_assert) { - // std.debug.assert(buf.items.len > 0 and buf.items[buf.items.len - 1] == 0); - // } - - // if (this.out == .array_of_slice) { - // this.out.array_of_slice.append(buf.items[0 .. buf.items.len - 1 :0]) catch bun.outOfMemory(); - // return; - // } - - // this.out.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.items.ptr))) catch bun.outOfMemory(); - } - - fn expandVar(this: *const Expansion, label: []const u8) EnvStr { - const value = this.base.shell.shell_env.get(EnvStr.initSlice(label)) orelse brk: { - break :brk this.base.shell.export_env.get(EnvStr.initSlice(label)) orelse return EnvStr.initSlice(""); - }; - return value; - } + unreachable; + } - fn currentWord(this: *Expansion) *const ast.SimpleAtom { - return switch (this.node) { - .simple => &this.node.simple, - .compound => &this.node.compound.atoms[this.word_idx], - }; + fn onGlobWalkDone(this: *Expansion, task: *ShellGlobTask) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.child_state == .glob); } - /// Returns the size of the atom when expanded. - /// If the calculation cannot be computed trivially (cmd substitution, brace expansion), this value is not accurate and `has_unknown` is set to true - fn expansionSizeHint(this: *const Expansion, atom: *const ast.Atom, has_unknown: *bool) usize { - return switch (@as(ast.Atom.Tag, atom.*)) { - .simple => this.expansionSizeHintSimple(&atom.simple, has_unknown), - .compound => { - if (atom.compound.brace_expansion_hint) { - has_unknown.* = true; - } - - var out: usize = 0; - for (atom.compound.atoms) |*simple| { - out += this.expansionSizeHintSimple(simple, has_unknown); - } - return out; + if (task.err) |*err| { + switch (err.*) { + .syscall => { + this.base.throw(&bun.shell.ShellErr.newSys(task.err.?.syscall)); }, - }; + .unknown => |errtag| { + this.base.throw(&.{ + .custom = bun.default_allocator.dupe(u8, @errorName(errtag)) catch bun.outOfMemory(), + }); + }, + } } - fn expansionSizeHintSimple(this: *const Expansion, simple: *const ast.SimpleAtom, has_cmd_subst: *bool) usize { - return switch (simple.*) { - .Text => |txt| txt.len, - .Var => |label| this.expandVar(label).len, - .brace_begin, .brace_end, .comma, .asterisk => 1, - .double_asterisk => 2, - .cmd_subst => |subst| { - _ = subst; // autofix - - // TODO check if the command substitution is comprised entirely of assignments or zero-sized things - // if (@as(ast.CmdOrAssigns.Tag, subst.*) == .assigns) { - // return 0; - // } - has_cmd_subst.* = true; - return 0; + if (task.result.items.len == 0) { + const msg = std.fmt.allocPrint(bun.default_allocator, "no matches found: {s}", .{this.child_state.glob.walker.pattern}) catch bun.outOfMemory(); + this.state = .{ + .err = bun.shell.ShellErr{ + .custom = msg, }, }; + this.child_state.glob.walker.deinit(true); + this.child_state = .idle; + this.next(); + return; } - fn outEnsureUnusedCapacity(this: *Expansion, additional: usize) void { - switch (this.out) { - .array_of_ptr => { - this.out.array_of_ptr.ensureUnusedCapacity(additional) catch bun.outOfMemory(); - }, - .array_of_slice => { - this.out.array_of_slice.ensureUnusedCapacity(additional) catch bun.outOfMemory(); - }, - .single => {}, - } + for (task.result.items) |sentinel_str| { + // The string is allocated in the glob walker arena and will be freed, so needs to be duped here + const duped = this.base.interpreter.allocator.dupeZ(u8, sentinel_str[0..sentinel_str.len]) catch bun.outOfMemory(); + this.pushResultSlice(duped); } - pub const ShellGlobTask = struct { - const print = bun.Output.scoped(.ShellGlobTask, false); + this.word_idx += 1; + this.child_state.glob.walker.deinit(true); + this.child_state = .idle; + this.state = .done; + this.next(); + } - task: WorkPoolTask = .{ .callback = &runFromThreadPool }, + /// If the atom is actually a command substitution then does nothing and returns true + pub fn expandSimpleNoIO(this: *Expansion, atom: *const ast.SimpleAtom, str_list: *std.ArrayList(u8)) bool { + switch (atom.*) { + .Text => |txt| { + str_list.appendSlice(txt) catch bun.outOfMemory(); + }, + .Var => |label| { + str_list.appendSlice(this.expandVar(label).slice()) catch bun.outOfMemory(); + }, + .asterisk => { + str_list.append('*') catch bun.outOfMemory(); + }, + .double_asterisk => { + str_list.appendSlice("**") catch bun.outOfMemory(); + }, + .brace_begin => { + str_list.append('{') catch bun.outOfMemory(); + }, + .brace_end => { + str_list.append('}') catch bun.outOfMemory(); + }, + .comma => { + str_list.append(',') catch bun.outOfMemory(); + }, + .cmd_subst => { + // TODO: + // if the command substution is comprised of solely shell variable assignments then it should do nothing + // if (atom.cmd_subst.* == .assigns) return false; + return true; + }, + } + return false; + } - /// Not owned by this struct - expansion: *Expansion, - /// Not owned by this struct - walker: *GlobWalker, + pub fn appendSlice(this: *Expansion, buf: *std.ArrayList(u8), slice: []const u8) void { + _ = this; + buf.appendSlice(slice) catch bun.outOfMemory(); + } - result: std.ArrayList([:0]const u8), - allocator: Allocator, - event_loop: EventLoopRef, - concurrent_task: EventLoopTask = .{}, - // This is a poll because we want it to enter the uSockets loop - ref: bun.Async.KeepAlive = .{}, - err: ?Err = null, + pub fn pushResultSlice(this: *Expansion, buf: [:0]const u8) void { + this.out.pushResultSlice(buf); + // if (comptime bun.Environment.allow_assert) { + // std.debug.assert(buf.len > 0 and buf[buf.len] == 0); + // } - const This = @This(); + // if (this.out == .array_of_slice) { + // this.out.array_of_slice.append(buf) catch bun.outOfMemory(); + // return; + // } - pub const event_loop_kind = EventLoopKind; + // this.out.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.ptr))) catch bun.outOfMemory(); + } - pub const Err = union(enum) { - syscall: Syscall.Error, - unknown: anyerror, + pub fn pushCurrentOut(this: *Expansion) void { + if (this.current_out.items.len == 0) return; + if (this.current_out.items[this.current_out.items.len - 1] != 0) this.current_out.append(0) catch bun.outOfMemory(); + this.pushResult(&this.current_out); + this.current_out = std.ArrayList(u8).init(this.base.interpreter.allocator); + } - pub fn toJSC(this: Err, globalThis: *JSGlobalObject) JSValue { - return switch (this) { - .syscall => |err| err.toJSC(globalThis), - .unknown => |err| JSC.ZigString.fromBytes(@errorName(err)).toValueGC(globalThis), - }; - } - }; + pub fn pushResult(this: *Expansion, buf: *std.ArrayList(u8)) void { + this.out.pushResult(buf); + // if (comptime bun.Environment.allow_assert) { + // std.debug.assert(buf.items.len > 0 and buf.items[buf.items.len - 1] == 0); + // } - pub fn createOnMainThread(allocator: Allocator, walker: *GlobWalker, expansion: *Expansion) *This { - print("createOnMainThread", .{}); - var this = allocator.create(This) catch bun.outOfMemory(); - this.* = .{ - .event_loop = event_loop_ref.get(), - .walker = walker, - .allocator = allocator, - .expansion = expansion, - .result = std.ArrayList([:0]const u8).init(allocator), - }; - // this.ref.ref(this.event_loop.virtual_machine); - this.ref.ref(event_loop_ref.get().getVmImpl()); + // if (this.out == .array_of_slice) { + // this.out.array_of_slice.append(buf.items[0 .. buf.items.len - 1 :0]) catch bun.outOfMemory(); + // return; + // } - return this; - } + // this.out.array_of_ptr.append(@as([*:0]const u8, @ptrCast(buf.items.ptr))) catch bun.outOfMemory(); + } - pub fn runFromThreadPool(task: *WorkPoolTask) void { - print("runFromThreadPool", .{}); - var this = @fieldParentPtr(This, "task", task); - switch (this.walkImpl()) { - .result => {}, - .err => |e| { - this.err = .{ .syscall = e }; - }, - } - this.onFinish(); - } + fn expandVar(this: *const Expansion, label: []const u8) EnvStr { + const value = this.base.shell.shell_env.get(EnvStr.initSlice(label)) orelse brk: { + break :brk this.base.shell.export_env.get(EnvStr.initSlice(label)) orelse return EnvStr.initSlice(""); + }; + return value; + } - fn walkImpl(this: *This) Maybe(void) { - print("walkImpl", .{}); + fn currentWord(this: *Expansion) *const ast.SimpleAtom { + return switch (this.node) { + .simple => &this.node.simple, + .compound => &this.node.compound.atoms[this.word_idx], + }; + } - var iter = GlobWalker.Iterator{ .walker = this.walker }; - defer iter.deinit(); - switch (try iter.init()) { - .err => |err| return .{ .err = err }, - else => {}, + /// Returns the size of the atom when expanded. + /// If the calculation cannot be computed trivially (cmd substitution, brace expansion), this value is not accurate and `has_unknown` is set to true + fn expansionSizeHint(this: *const Expansion, atom: *const ast.Atom, has_unknown: *bool) usize { + return switch (@as(ast.Atom.Tag, atom.*)) { + .simple => this.expansionSizeHintSimple(&atom.simple, has_unknown), + .compound => { + if (atom.compound.brace_expansion_hint) { + has_unknown.* = true; } - while (switch (iter.next() catch |e| OOM(e)) { - .err => |err| return .{ .err = err }, - .result => |matched_path| matched_path, - }) |path| { - this.result.append(path) catch bun.outOfMemory(); + var out: usize = 0; + for (atom.compound.atoms) |*simple| { + out += this.expansionSizeHintSimple(simple, has_unknown); } + return out; + }, + }; + } - return Maybe(void).success; - } - - pub fn runFromMainThread(this: *This) void { - print("runFromJS", .{}); - this.expansion.onGlobWalkDone(this); - // this.ref.unref(this.event_loop.virtual_machine); - this.ref.unref(this.event_loop.getVmImpl()); - } + fn expansionSizeHintSimple(this: *const Expansion, simple: *const ast.SimpleAtom, has_cmd_subst: *bool) usize { + return switch (simple.*) { + .Text => |txt| txt.len, + .Var => |label| this.expandVar(label).len, + .brace_begin, .brace_end, .comma, .asterisk => 1, + .double_asterisk => 2, + .cmd_subst => |subst| { + _ = subst; // autofix + + // TODO check if the command substitution is comprised entirely of assignments or zero-sized things + // if (@as(ast.CmdOrAssigns.Tag, subst.*) == .assigns) { + // return 0; + // } + has_cmd_subst.* = true; + return 0; + }, + }; + } - pub fn runFromMainThreadMini(this: *This, _: *void) void { - this.runFromMainThread(); - } + fn outEnsureUnusedCapacity(this: *Expansion, additional: usize) void { + switch (this.out) { + .array_of_ptr => { + this.out.array_of_ptr.ensureUnusedCapacity(additional) catch bun.outOfMemory(); + }, + .array_of_slice => { + this.out.array_of_slice.ensureUnusedCapacity(additional) catch bun.outOfMemory(); + }, + .single => {}, + } + } - pub fn schedule(this: *This) void { - print("schedule", .{}); - WorkPool.schedule(&this.task); - } + pub const ShellGlobTask = struct { + const print = bun.Output.scoped(.ShellGlobTask, false); - pub fn onFinish(this: *This) void { - print("onFinish", .{}); - if (comptime EventLoopKind == .js) { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } else { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, "runFromMainThreadMini")); - } - } + task: WorkPoolTask = .{ .callback = &runFromThreadPool }, - pub fn deinit(this: *This) void { - print("deinit", .{}); - this.result.deinit(); - this.allocator.destroy(this); - } - }; - }; + /// Not owned by this struct + expansion: *Expansion, + /// Not owned by this struct + walker: *GlobWalker, - pub const State = struct { - kind: StateKind, - interpreter: *ThisInterpreter, - shell: *ShellState, - }; + result: std.ArrayList([:0]const u8), + allocator: Allocator, + event_loop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, + // This is a poll because we want it to enter the uSockets loop + ref: bun.Async.KeepAlive = .{}, + err: ?Err = null, - pub const Script = struct { - base: State, - node: *const ast.Script, - // currently_executing: ?ChildPtr, - io: ?IO = null, - parent: ParentPtr, - state: union(enum) { - normal: struct { - idx: usize = 0, - }, - } = .{ .normal = .{} }, + const This = @This(); - pub const ParentPtr = StatePtrUnion(.{ - ThisInterpreter, - Expansion, - }); + pub const Err = union(enum) { + syscall: Syscall.Error, + unknown: anyerror, - pub const ChildPtr = struct { - val: *Stmt, - pub inline fn init(child: *Stmt) ChildPtr { - return .{ .val = child }; - } - pub inline fn deinit(this: ChildPtr) void { - this.val.deinit(); + pub fn toJSC(this: Err, globalThis: *JSGlobalObject) JSValue { + return switch (this) { + .syscall => |err| err.toJSC(globalThis), + .unknown => |err| JSC.ZigString.fromBytes(@errorName(err)).toValueGC(globalThis), + }; } }; - fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: *const ast.Script, - parent_ptr: ParentPtr, - io: ?IO, - ) *Script { - const script = interpreter.allocator.create(Script) catch bun.outOfMemory(); - script.* = .{ - .base = .{ .kind = .script, .interpreter = interpreter, .shell = shell_state }, - .node = node, - .parent = parent_ptr, - .io = io, + pub fn createOnMainThread(allocator: Allocator, walker: *GlobWalker, expansion: *Expansion) *This { + print("createOnMainThread", .{}); + var this = allocator.create(This) catch bun.outOfMemory(); + this.* = .{ + .event_loop = expansion.base.eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(expansion.base.eventLoop()), + .walker = walker, + .allocator = allocator, + .expansion = expansion, + .result = std.ArrayList([:0]const u8).init(allocator), }; - return script; - } - fn getIO(this: *Script) IO { - if (this.io) |io| return io; - return this.base.shell.io; - } + // this.ref.ref(this.event_loop.virtual_machine); + this.ref.ref(this.event_loop); - fn start(this: *Script) void { - if (this.node.stmts.len == 0) - return this.finish(0); - this.next(); + return this; } - fn next(this: *Script) void { - switch (this.state) { - .normal => { - if (this.state.normal.idx >= this.node.stmts.len) return; - const stmt_node = &this.node.stmts[this.state.normal.idx]; - this.state.normal.idx += 1; - var stmt = Stmt.init(this.base.interpreter, this.base.shell, stmt_node, this, this.getIO()) catch bun.outOfMemory(); - stmt.start(); - return; + pub fn runFromThreadPool(task: *WorkPoolTask) void { + print("runFromThreadPool", .{}); + var this = @fieldParentPtr(This, "task", task); + switch (this.walkImpl()) { + .result => {}, + .err => |e| { + this.err = .{ .syscall = e }; }, } + this.onFinish(); } - fn finish(this: *Script, exit_code: ExitCode) void { - if (this.parent.ptr.is(ThisInterpreter)) { - log("SCRIPT DONE YO!", .{}); - // this.base.interpreter.finish(exit_code); - this.base.interpreter.childDone(InterpreterChildPtr.init(this), exit_code); - return; + fn walkImpl(this: *This) Maybe(void) { + print("walkImpl", .{}); + + var iter = GlobWalker.Iterator{ .walker = this.walker }; + defer iter.deinit(); + switch (try iter.init()) { + .err => |err| return .{ .err = err }, + else => {}, } - if (this.parent.ptr.is(Expansion)) { - this.parent.childDone(this, exit_code); - return; + while (switch (iter.next() catch |e| OOM(e)) { + .err => |err| return .{ .err = err }, + .result => |matched_path| matched_path, + }) |path| { + this.result.append(path) catch bun.outOfMemory(); } + + return Maybe(void).success; } - fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) void { - child.deinit(); - if (this.state.normal.idx >= this.node.stmts.len) { - this.finish(exit_code); - return; - } - this.next(); + pub fn runFromMainThread(this: *This) void { + print("runFromJS", .{}); + this.expansion.onGlobWalkDone(this); + // this.ref.unref(this.event_loop.virtual_machine); + this.ref.unref(this.event_loop); } - pub fn deinit(this: *Script) void { - if (this.parent.ptr.is(ThisInterpreter)) { - return; - } + pub fn runFromMainThreadMini(this: *This, _: *void) void { + this.runFromMainThread(); + } - this.base.shell.deinit(); - bun.default_allocator.destroy(this); + pub fn schedule(this: *This) void { + print("schedule", .{}); + WorkPool.schedule(&this.task); } - pub fn deinitFromInterpreter(this: *Script) void { - // Let the interpreter deinitialize the shell state - // this.base.shell.deinitImpl(false, false); - bun.default_allocator.destroy(this); + pub fn onFinish(this: *This) void { + print("onFinish", .{}); + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } + + pub fn deinit(this: *This) void { + print("deinit", .{}); + this.result.deinit(); + this.allocator.destroy(this); } }; + }; - /// In pipelines and conditional expressions, assigns (e.g. `FOO=bar BAR=baz && - /// echo hi` or `FOO=bar BAR=baz | echo hi`) have no effect on the environment - /// of the shell, so we can skip them. - const Assigns = struct { - base: State, - node: []const ast.Assign, - parent: ParentPtr, - state: union(enum) { - idle, - expanding: struct { - idx: u32 = 0, - current_expansion_result: std.ArrayList([:0]const u8), - expansion: Expansion, - }, - err: bun.shell.ShellErr, - done, + pub const State = struct { + kind: StateKind, + interpreter: *ThisInterpreter, + shell: *ShellState, + + pub inline fn eventLoop(this: *const State) JSC.EventLoopHandle { + return this.interpreter.event_loop; + } + + pub fn throw(this: *const State, err: *const bun.shell.ShellErr) void { + throwShellErr(err, this.eventLoop()); + } + + pub fn rootIO(this: *const State) *const IO { + return this.interpreter.rootIO(); + } + }; + + pub const Script = struct { + base: State, + node: *const ast.Script, + // currently_executing: ?ChildPtr, + io: IO, + parent: ParentPtr, + state: union(enum) { + normal: struct { + idx: usize = 0, }, - ctx: AssignCtx, + } = .{ .normal = .{} }, - const ParentPtr = StatePtrUnion(.{ - Stmt, - Cond, - Cmd, - Pipeline, - }); + pub const ParentPtr = StatePtrUnion(.{ + ThisInterpreter, + Expansion, + }); - const ChildPtr = StatePtrUnion(.{ - Expansion, - }); + pub const ChildPtr = struct { + val: *Stmt, + pub inline fn init(child: *Stmt) ChildPtr { + return .{ .val = child }; + } + pub inline fn deinit(this: ChildPtr) void { + this.val.deinit(); + } + }; - pub inline fn deinit(this: *Assigns) void { - if (this.state == .expanding) { - this.state.expanding.current_expansion_result.deinit(); - } + fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, + node: *const ast.Script, + parent_ptr: ParentPtr, + io: IO, + ) *Script { + const script = interpreter.allocator.create(Script) catch bun.outOfMemory(); + script.* = .{ + .base = .{ .kind = .script, .interpreter = interpreter, .shell = shell_state }, + .node = node, + .parent = parent_ptr, + .io = io, + }; + log("Script(0x{x}) init", .{@intFromPtr(script)}); + return script; + } + + fn getIO(this: *Script) IO { + return this.io; + } + + fn start(this: *Script) void { + if (this.node.stmts.len == 0) + return this.finish(0); + this.next(); + } + + fn next(this: *Script) void { + switch (this.state) { + .normal => { + if (this.state.normal.idx >= this.node.stmts.len) return; + const stmt_node = &this.node.stmts[this.state.normal.idx]; + this.state.normal.idx += 1; + var io = this.getIO(); + var stmt = Stmt.init(this.base.interpreter, this.base.shell, stmt_node, this, io.ref().*) catch bun.outOfMemory(); + stmt.start(); + return; + }, } + } - pub inline fn start(this: *Assigns) void { - return this.next(); + fn finish(this: *Script, exit_code: ExitCode) void { + if (this.parent.ptr.is(ThisInterpreter)) { + log("SCRIPT DONE YO!", .{}); + // this.base.interpreter.finish(exit_code); + this.base.interpreter.childDone(InterpreterChildPtr.init(this), exit_code); + return; } - pub fn init( - this: *Assigns, - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: []const ast.Assign, - ctx: AssignCtx, - parent: ParentPtr, - ) void { - this.* = .{ - .base = .{ .kind = .assign, .interpreter = interpreter, .shell = shell_state }, - .node = node, - .parent = parent, - .state = .idle, - .ctx = ctx, - }; + if (this.parent.ptr.is(Expansion)) { + this.parent.childDone(this, exit_code); + return; } + } - pub fn next(this: *Assigns) void { - while (!(this.state == .done)) { - switch (this.state) { - .idle => { - this.state = .{ .expanding = .{ - .current_expansion_result = std.ArrayList([:0]const u8).init(bun.default_allocator), - .expansion = undefined, - } }; - continue; - }, - .expanding => { - if (this.state.expanding.idx >= this.node.len) { - this.state = .done; - continue; - } + fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) void { + child.deinit(); + if (this.state.normal.idx >= this.node.stmts.len) { + this.finish(exit_code); + return; + } + this.next(); + } - Expansion.init( - this.base.interpreter, - this.base.shell, - &this.state.expanding.expansion, - &this.node[this.state.expanding.idx].value, - Expansion.ParentPtr.init(this), - .{ - .array_of_slice = &this.state.expanding.current_expansion_result, - }, - ); - this.state.expanding.expansion.start(); - return; - }, - .done => unreachable, - .err => return this.parent.childDone(this, 1), - } - } + pub fn deinit(this: *Script) void { + log("Script(0x{x}) deinit", .{@intFromPtr(this)}); + this.io.deref(); + if (this.parent.ptr.is(ThisInterpreter)) { + return; + } - this.parent.childDone(this, 0); + this.base.shell.deinit(); + bun.default_allocator.destroy(this); + } + + pub fn deinitFromInterpreter(this: *Script) void { + log("Script(0x{x}) deinitFromInterpreter", .{@intFromPtr(this)}); + // Let the interpreter deinitialize the shell state + this.io.deinit(); + // this.base.shell.deinitImpl(false, false); + bun.default_allocator.destroy(this); + } + }; + + /// In pipelines and conditional expressions, assigns (e.g. `FOO=bar BAR=baz && + /// echo hi` or `FOO=bar BAR=baz | echo hi`) have no effect on the environment + /// of the shell, so we can skip them. + const Assigns = struct { + base: State, + node: []const ast.Assign, + parent: ParentPtr, + state: union(enum) { + idle, + expanding: struct { + idx: u32 = 0, + current_expansion_result: std.ArrayList([:0]const u8), + expansion: Expansion, + }, + err: bun.shell.ShellErr, + done, + }, + ctx: AssignCtx, + io: IO, + + const ParentPtr = StatePtrUnion(.{ + Stmt, + Cond, + Cmd, + Pipeline, + }); + + const ChildPtr = StatePtrUnion(.{ + Expansion, + }); + + pub inline fn deinit(this: *Assigns) void { + if (this.state == .expanding) { + this.state.expanding.current_expansion_result.deinit(); } + this.io.deinit(); + } - pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { - if (child.ptr.is(Expansion)) { - const expansion = child.ptr.as(Expansion); - if (exit_code != 0) { - this.state = .{ - .err = expansion.state.err, - }; + pub inline fn start(this: *Assigns) void { + return this.next(); + } + + pub fn init( + this: *Assigns, + interpreter: *ThisInterpreter, + shell_state: *ShellState, + node: []const ast.Assign, + ctx: AssignCtx, + parent: ParentPtr, + io: IO, + ) void { + this.* = .{ + .base = .{ .kind = .assign, .interpreter = interpreter, .shell = shell_state }, + .node = node, + .parent = parent, + .state = .idle, + .ctx = ctx, + .io = io, + }; + } + + pub fn next(this: *Assigns) void { + while (!(this.state == .done)) { + switch (this.state) { + .idle => { + this.state = .{ .expanding = .{ + .current_expansion_result = std.ArrayList([:0]const u8).init(bun.default_allocator), + .expansion = undefined, + } }; + continue; + }, + .expanding => { + if (this.state.expanding.idx >= this.node.len) { + this.state = .done; + continue; + } + + Expansion.init( + this.base.interpreter, + this.base.shell, + &this.state.expanding.expansion, + &this.node[this.state.expanding.idx].value, + Expansion.ParentPtr.init(this), + .{ + .array_of_slice = &this.state.expanding.current_expansion_result, + }, + this.io.copy(), + ); + this.state.expanding.expansion.start(); return; - } - var expanding = &this.state.expanding; + }, + .done => unreachable, + .err => return this.parent.childDone(this, 1), + } + } - const label = this.node[expanding.idx].label; + this.parent.childDone(this, 0); + } - if (expanding.current_expansion_result.items.len == 1) { - const value = expanding.current_expansion_result.items[0]; - const ref = EnvStr.initRefCounted(value); - defer ref.deref(); - this.base.shell.assignVar(this.base.interpreter, EnvStr.initSlice(label), ref, this.ctx); - expanding.current_expansion_result = std.ArrayList([:0]const u8).init(bun.default_allocator); - } else { - const size = brk: { - var total: usize = 0; - for (expanding.current_expansion_result.items) |slice| { - total += slice.len; - } - break :brk total; - }; + pub fn childDone(this: *Assigns, child: ChildPtr, exit_code: ExitCode) void { + if (child.ptr.is(Expansion)) { + const expansion = child.ptr.as(Expansion); + if (exit_code != 0) { + this.state = .{ + .err = expansion.state.err, + }; + expansion.deinit(); + return; + } + var expanding = &this.state.expanding; - const value = brk: { - var merged = bun.default_allocator.allocSentinel(u8, size, 0) catch bun.outOfMemory(); - var i: usize = 0; - for (expanding.current_expansion_result.items) |slice| { - @memcpy(merged[i .. i + slice.len], slice[0..slice.len]); - i += slice.len; - } - break :brk merged; - }; - const value_ref = EnvStr.initRefCounted(value); - defer value_ref.deref(); + const label = this.node[expanding.idx].label; + + if (expanding.current_expansion_result.items.len == 1) { + const value = expanding.current_expansion_result.items[0]; + const ref = EnvStr.initRefCounted(value); + defer ref.deref(); + this.base.shell.assignVar(this.base.interpreter, EnvStr.initSlice(label), ref, this.ctx); + expanding.current_expansion_result = std.ArrayList([:0]const u8).init(bun.default_allocator); + } else { + const size = brk: { + var total: usize = 0; + for (expanding.current_expansion_result.items) |slice| { + total += slice.len; + } + break :brk total; + }; - this.base.shell.assignVar(this.base.interpreter, EnvStr.initSlice(label), value_ref, this.ctx); + const value = brk: { + var merged = bun.default_allocator.allocSentinel(u8, size, 0) catch bun.outOfMemory(); + var i: usize = 0; for (expanding.current_expansion_result.items) |slice| { - bun.default_allocator.free(slice); + @memcpy(merged[i .. i + slice.len], slice[0..slice.len]); + i += slice.len; } - expanding.current_expansion_result.clearRetainingCapacity(); - } + break :brk merged; + }; + const value_ref = EnvStr.initRefCounted(value); + defer value_ref.deref(); - expanding.idx += 1; - this.next(); - return; + this.base.shell.assignVar(this.base.interpreter, EnvStr.initSlice(label), value_ref, this.ctx); + for (expanding.current_expansion_result.items) |slice| { + bun.default_allocator.free(slice); + } + expanding.current_expansion_result.clearRetainingCapacity(); } - unreachable; + expanding.idx += 1; + expansion.deinit(); + this.next(); + return; } - }; - pub const Stmt = struct { - base: State, + unreachable; + } + }; + + pub const Stmt = struct { + base: State, + node: *const ast.Stmt, + parent: *Script, + idx: usize, + last_exit_code: ?ExitCode, + currently_executing: ?ChildPtr, + io: IO, + // state: union(enum) { + // idle, + // wait_child, + // child_done, + // done, + // }, + + const ChildPtr = StatePtrUnion(.{ + Cond, + Pipeline, + Cmd, + Assigns, + }); + + pub fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, node: *const ast.Stmt, parent: *Script, - idx: usize, - last_exit_code: ?ExitCode, - currently_executing: ?ChildPtr, io: IO, - // state: union(enum) { - // idle, - // wait_child, - // child_done, - // done, - // }, - - const ChildPtr = StatePtrUnion(.{ - Cond, - Pipeline, - Cmd, - Assigns, - }); + ) !*Stmt { + var script = try interpreter.allocator.create(Stmt); + script.base = .{ .kind = .stmt, .interpreter = interpreter, .shell = shell_state }; + script.node = node; + script.parent = parent; + script.idx = 0; + script.last_exit_code = null; + script.currently_executing = null; + script.io = io; + log("Stmt(0x{x}) init", .{@intFromPtr(script)}); + return script; + } - pub fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: *const ast.Stmt, - parent: *Script, - io: IO, - ) !*Stmt { - var script = try interpreter.allocator.create(Stmt); - script.base = .{ .kind = .stmt, .interpreter = interpreter, .shell = shell_state }; - script.node = node; - script.parent = parent; - script.idx = 0; - script.last_exit_code = null; - script.currently_executing = null; - script.io = io; - return script; - } - - // pub fn next(this: *Stmt) void { - // _ = this; - // } + // pub fn next(this: *Stmt) void { + // _ = this; + // } - pub fn start(this: *Stmt) void { - if (bun.Environment.allow_assert) { - std.debug.assert(this.idx == 0); - std.debug.assert(this.last_exit_code == null); - std.debug.assert(this.currently_executing == null); - } - this.next(); + pub fn start(this: *Stmt) void { + if (bun.Environment.allow_assert) { + std.debug.assert(this.idx == 0); + std.debug.assert(this.last_exit_code == null); + std.debug.assert(this.currently_executing == null); } + this.next(); + } - pub fn next(this: *Stmt) void { - if (this.idx >= this.node.exprs.len) - return this.parent.childDone(Script.ChildPtr.init(this), this.last_exit_code orelse 0); + pub fn next(this: *Stmt) void { + if (this.idx >= this.node.exprs.len) + return this.parent.childDone(Script.ChildPtr.init(this), this.last_exit_code orelse 0); - const child = &this.node.exprs[this.idx]; - switch (child.*) { - .cond => { - const cond = Cond.init(this.base.interpreter, this.base.shell, child.cond, Cond.ParentPtr.init(this), this.io); - this.currently_executing = ChildPtr.init(cond); - cond.start(); - }, - .cmd => { - const cmd = Cmd.init(this.base.interpreter, this.base.shell, child.cmd, Cmd.ParentPtr.init(this), this.io); - this.currently_executing = ChildPtr.init(cmd); - cmd.start(); - }, - .pipeline => { - const pipeline = Pipeline.init(this.base.interpreter, this.base.shell, child.pipeline, Pipeline.ParentPtr.init(this), this.io); - this.currently_executing = ChildPtr.init(pipeline); - pipeline.start(); - }, - .assign => |assigns| { - var assign_machine = this.base.interpreter.allocator.create(Assigns) catch bun.outOfMemory(); - assign_machine.init(this.base.interpreter, this.base.shell, assigns, .shell, Assigns.ParentPtr.init(this)); - assign_machine.start(); - }, - .subshell => { - @panic(SUBSHELL_TODO_ERROR); - }, - } + const child = &this.node.exprs[this.idx]; + switch (child.*) { + .cond => { + const cond = Cond.init(this.base.interpreter, this.base.shell, child.cond, Cond.ParentPtr.init(this), this.io.copy()); + this.currently_executing = ChildPtr.init(cond); + cond.start(); + }, + .cmd => { + const cmd = Cmd.init(this.base.interpreter, this.base.shell, child.cmd, Cmd.ParentPtr.init(this), this.io.copy()); + this.currently_executing = ChildPtr.init(cmd); + cmd.start(); + }, + .pipeline => { + const pipeline = Pipeline.init(this.base.interpreter, this.base.shell, child.pipeline, Pipeline.ParentPtr.init(this), this.io.copy()); + this.currently_executing = ChildPtr.init(pipeline); + pipeline.start(); + }, + .assign => |assigns| { + var assign_machine = this.base.interpreter.allocator.create(Assigns) catch bun.outOfMemory(); + assign_machine.init(this.base.interpreter, this.base.shell, assigns, .shell, Assigns.ParentPtr.init(this), this.io.copy()); + assign_machine.start(); + }, + .subshell => { + @panic(SUBSHELL_TODO_ERROR); + }, } + } + + pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) void { + const data = child.ptr.repr.data; + log("child done Stmt {x} child({s})={x} exit={d}", .{ @intFromPtr(this), child.tagName(), @as(usize, @intCast(child.ptr.repr._ptr)), exit_code }); + this.last_exit_code = exit_code; + this.idx += 1; + const data2 = child.ptr.repr.data; + log("{d} {d}", .{ data, data2 }); + child.deinit(); + this.currently_executing = null; + this.next(); + } - pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) void { - const data = child.ptr.repr.data; - log("child done Stmt {x} child({s})={x} exit={d}", .{ @intFromPtr(this), child.tagName(), @as(usize, @intCast(child.ptr.repr._ptr)), exit_code }); - this.last_exit_code = exit_code; - this.idx += 1; - const data2 = child.ptr.repr.data; - log("{d} {d}", .{ data, data2 }); + pub fn deinit(this: *Stmt) void { + log("Stmt(0x{x}) deinit", .{@intFromPtr(this)}); + this.io.deinit(); + if (this.currently_executing) |child| { child.deinit(); - this.currently_executing = null; - this.next(); } + this.base.interpreter.allocator.destroy(this); + } + }; - pub fn deinit(this: *Stmt) void { - if (this.currently_executing) |child| { - child.deinit(); - } - this.base.interpreter.allocator.destroy(this); - } - }; + pub const Cond = struct { + base: State, + node: *const ast.Conditional, + /// Based on precedence rules conditional can only be child of a stmt or + /// another conditional + parent: ParentPtr, + left: ?ExitCode = null, + right: ?ExitCode = null, + io: IO, + currently_executing: ?ChildPtr = null, + + const ChildPtr = StatePtrUnion(.{ + Cmd, + Pipeline, + Cond, + Assigns, + }); + + const ParentPtr = StatePtrUnion(.{ + Stmt, + Cond, + }); - pub const Cond = struct { - base: State, + pub fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, node: *const ast.Conditional, - /// Based on precedence rules conditional can only be child of a stmt or - /// another conditional parent: ParentPtr, - left: ?ExitCode = null, - right: ?ExitCode = null, io: IO, - currently_executing: ?ChildPtr = null, + ) *Cond { + var cond = interpreter.allocator.create(Cond) catch |err| { + std.debug.print("Ruh roh: {any}\n", .{err}); + @panic("Ruh roh"); + }; + cond.node = node; + cond.base = .{ .kind = .cond, .interpreter = interpreter, .shell = shell_state }; + cond.parent = parent; + cond.io = io; + cond.left = null; + cond.right = null; + cond.currently_executing = null; + return cond; + } - const ChildPtr = StatePtrUnion(.{ - Cmd, - Pipeline, - Cond, - Assigns, - }); + fn start(this: *Cond) void { + log("conditional start {x} ({s})", .{ @intFromPtr(this), @tagName(this.node.op) }); + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.left == null); + std.debug.assert(this.right == null); + std.debug.assert(this.currently_executing == null); + } - const ParentPtr = StatePtrUnion(.{ - Stmt, - Cond, - }); + this.currently_executing = this.makeChild(true); + if (this.currently_executing == null) { + this.currently_executing = this.makeChild(false); + this.left = 0; + } + if (this.currently_executing) |exec| { + exec.start(); + } + // var child = this.currently_executing.?.as(Cmd); + // child.start(); + } - pub fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: *const ast.Conditional, - parent: ParentPtr, - io: IO, - ) *Cond { - var cond = interpreter.allocator.create(Cond) catch |err| { - std.debug.print("Ruh roh: {any}\n", .{err}); - @panic("Ruh roh"); - }; - cond.node = node; - cond.base = .{ .kind = .cond, .interpreter = interpreter, .shell = shell_state }; - cond.parent = parent; - cond.io = io; - cond.left = null; - cond.right = null; - cond.currently_executing = null; - return cond; - } - - fn start(this: *Cond) void { - log("conditional start {x} ({s})", .{ @intFromPtr(this), @tagName(this.node.op) }); - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.left == null); - std.debug.assert(this.right == null); - std.debug.assert(this.currently_executing == null); - } + /// Returns null if child is assignments + fn makeChild(this: *Cond, left: bool) ?ChildPtr { + const node = if (left) &this.node.left else &this.node.right; + switch (node.*) { + .cmd => { + const cmd = Cmd.init(this.base.interpreter, this.base.shell, node.cmd, Cmd.ParentPtr.init(this), this.io.copy()); + return ChildPtr.init(cmd); + }, + .cond => { + const cond = Cond.init(this.base.interpreter, this.base.shell, node.cond, Cond.ParentPtr.init(this), this.io.copy()); + return ChildPtr.init(cond); + }, + .pipeline => { + const pipeline = Pipeline.init(this.base.interpreter, this.base.shell, node.pipeline, Pipeline.ParentPtr.init(this), this.io.copy()); + return ChildPtr.init(pipeline); + }, + .assign => |assigns| { + var assign_machine = this.base.interpreter.allocator.create(Assigns) catch bun.outOfMemory(); + assign_machine.init(this.base.interpreter, this.base.shell, assigns, .shell, Assigns.ParentPtr.init(this), this.io.copy()); + return ChildPtr.init(assign_machine); + }, + .subshell => @panic(SUBSHELL_TODO_ERROR), + } + } - this.currently_executing = this.makeChild(true); - if (this.currently_executing == null) { - this.currently_executing = this.makeChild(false); - this.left = 0; - } - if (this.currently_executing) |exec| { - exec.start(); - } - // var child = this.currently_executing.?.as(Cmd); - // child.start(); + pub fn childDone(this: *Cond, child: ChildPtr, exit_code: ExitCode) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.left == null or this.right == null); + std.debug.assert(this.currently_executing != null); } + log("conditional child done {x} ({s}) {s}", .{ @intFromPtr(this), @tagName(this.node.op), if (this.left == null) "left" else "right" }); - /// Returns null if child is assignments - fn makeChild(this: *Cond, left: bool) ?ChildPtr { - const node = if (left) &this.node.left else &this.node.right; - switch (node.*) { - .cmd => { - const cmd = Cmd.init(this.base.interpreter, this.base.shell, node.cmd, Cmd.ParentPtr.init(this), this.io); - return ChildPtr.init(cmd); - }, - .cond => { - const cond = Cond.init(this.base.interpreter, this.base.shell, node.cond, Cond.ParentPtr.init(this), this.io); - return ChildPtr.init(cond); - }, - .pipeline => { - const pipeline = Pipeline.init(this.base.interpreter, this.base.shell, node.pipeline, Pipeline.ParentPtr.init(this), this.io); - return ChildPtr.init(pipeline); - }, - .assign => |assigns| { - var assign_machine = this.base.interpreter.allocator.create(Assigns) catch bun.outOfMemory(); - assign_machine.init(this.base.interpreter, this.base.shell, assigns, .shell, Assigns.ParentPtr.init(this)); - return ChildPtr.init(assign_machine); - }, - .subshell => @panic(SUBSHELL_TODO_ERROR), + child.deinit(); + this.currently_executing = null; + + if (this.left == null) { + this.left = exit_code; + if ((this.node.op == .And and exit_code != 0) or (this.node.op == .Or and exit_code == 0)) { + this.parent.childDone(this, exit_code); + return; } - } - pub fn childDone(this: *Cond, child: ChildPtr, exit_code: ExitCode) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.left == null or this.right == null); - std.debug.assert(this.currently_executing != null); + this.currently_executing = this.makeChild(false); + if (this.currently_executing == null) { + this.right = 0; + this.parent.childDone(this, 0); + return; + } else { + this.currently_executing.?.start(); + // this.currently_executing.?.as(Cmd).start(); } - log("conditional child done {x} ({s}) {s}", .{ @intFromPtr(this), @tagName(this.node.op), if (this.left == null) "left" else "right" }); + return; + } + + this.right = exit_code; + this.parent.childDone(this, exit_code); + } + pub fn deinit(this: *Cond) void { + if (this.currently_executing) |child| { child.deinit(); - this.currently_executing = null; + } + this.base.interpreter.allocator.destroy(this); + } + }; - if (this.left == null) { - this.left = exit_code; - if ((this.node.op == .And and exit_code != 0) or (this.node.op == .Or and exit_code == 0)) { - this.parent.childDone(this, exit_code); - return; - } + pub const Pipeline = struct { + base: State, + node: *const ast.Pipeline, + /// Based on precedence rules pipeline can only be child of a stmt or + /// conditional + parent: ParentPtr, + exited_count: u32, + cmds: ?[]CmdOrResult, + pipes: ?[]Pipe, + io: IO, + state: union(enum) { + idle, + executing, + waiting_write_err, + done, + } = .idle, + + const TrackedFd = struct { + fd: bun.FileDescriptor, + open: bool = false, + }; - this.currently_executing = this.makeChild(false); - if (this.currently_executing == null) { - this.right = 0; - this.parent.childDone(this, 0); - return; - } else { - this.currently_executing.?.start(); - // this.currently_executing.?.as(Cmd).start(); - } - return; - } + const ParentPtr = StatePtrUnion(.{ + Stmt, + Cond, + }); - this.right = exit_code; - this.parent.childDone(this, exit_code); - } + const ChildPtr = StatePtrUnion(.{ + Cmd, + Assigns, + }); - pub fn deinit(this: *Cond) void { - if (this.currently_executing) |child| { - child.deinit(); - } - this.base.interpreter.allocator.destroy(this); - } + const CmdOrResult = union(enum) { + cmd: *Cmd, + result: ExitCode, }; - pub const Pipeline = struct { - base: State, + pub fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, node: *const ast.Pipeline, - /// Based on precedence rules pipeline can only be child of a stmt or - /// conditional parent: ParentPtr, - exited_count: u32, - cmds: ?[]CmdOrResult, - pipes: ?[]Pipe, - io: ?IO, - state: union(enum) { - idle, - executing, - waiting_write_err: BufferedWriter, - done, - } = .idle, - - const TrackedFd = struct { - fd: bun.FileDescriptor, - open: bool = false, + io: IO, + ) *Pipeline { + const pipeline = interpreter.allocator.create(Pipeline) catch bun.outOfMemory(); + pipeline.* = .{ + .base = .{ .kind = .pipeline, .interpreter = interpreter, .shell = shell_state }, + .node = node, + .parent = parent, + .exited_count = 0, + .cmds = null, + .pipes = null, + .io = io, }; - const ParentPtr = StatePtrUnion(.{ - Stmt, - Cond, - }); + return pipeline; + } - const ChildPtr = StatePtrUnion(.{ - Cmd, - Assigns, - }); + fn getIO(this: *Pipeline) IO { + return this.io; + } - const CmdOrResult = union(enum) { - cmd: *Cmd, - result: ExitCode, + fn writeFailingError(this: *Pipeline, comptime fmt: []const u8, args: anytype) void { + const handler = struct { + fn enqueueCb(ctx: *Pipeline) void { + ctx.state = .waiting_write_err; + } }; + this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + } - pub fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: *const ast.Pipeline, - parent: ParentPtr, - io: ?IO, - ) *Pipeline { - const pipeline = interpreter.allocator.create(Pipeline) catch bun.outOfMemory(); - pipeline.* = .{ - .base = .{ .kind = .pipeline, .interpreter = interpreter, .shell = shell_state }, - .node = node, - .parent = parent, - .exited_count = 0, - .cmds = null, - .pipes = null, - .io = io, - }; - - return pipeline; - } - - fn getIO(this: *Pipeline) IO { - return this.io orelse this.base.shell.io; - } - - fn setupCommands(this: *Pipeline) CoroutineResult { - const cmd_count = brk: { - var i: u32 = 0; - for (this.node.items) |*item| { - if (item.* == .cmd) i += 1; - } - break :brk i; - }; + fn setupCommands(this: *Pipeline) CoroutineResult { + const cmd_count = brk: { + var i: u32 = 0; + for (this.node.items) |*item| { + if (item.* == .cmd) i += 1; + } + break :brk i; + }; - this.cmds = if (cmd_count >= 1) this.base.interpreter.allocator.alloc(CmdOrResult, this.node.items.len) catch bun.outOfMemory() else null; - if (this.cmds == null) return .cont; - var pipes = this.base.interpreter.allocator.alloc(Pipe, if (cmd_count > 1) cmd_count - 1 else 1) catch bun.outOfMemory(); + this.cmds = if (cmd_count >= 1) this.base.interpreter.allocator.alloc(CmdOrResult, this.node.items.len) catch bun.outOfMemory() else null; + if (this.cmds == null) return .cont; + var pipes = this.base.interpreter.allocator.alloc(Pipe, if (cmd_count > 1) cmd_count - 1 else 1) catch bun.outOfMemory(); - if (cmd_count > 1) { - var pipes_set: u32 = 0; - if (Pipeline.initializePipes(pipes, &pipes_set).asErr()) |err| { - for (pipes[0..pipes_set]) |*pipe| { - closefd(pipe[0]); - closefd(pipe[1]); - } - const system_err = err.toSystemError(); - this.writeFailingError("bun: {s}\n", .{system_err.message}, 1); - return .yield; + if (cmd_count > 1) { + var pipes_set: u32 = 0; + if (Pipeline.initializePipes(pipes, &pipes_set).asErr()) |err| { + for (pipes[0..pipes_set]) |*pipe| { + closefd(pipe[0]); + closefd(pipe[1]); } + const system_err = err.toSystemError(); + this.writeFailingError("bun: {s}\n", .{system_err.message}); + return .yield; } + } - var i: u32 = 0; - for (this.node.items) |*item| { - switch (item.*) { - .cmd => { - const kind = "subproc"; - _ = kind; - var cmd_io = this.getIO(); - const stdin = if (cmd_count > 1) Pipeline.readPipe(pipes, i, &cmd_io) else cmd_io.stdin; - const stdout = if (cmd_count > 1) Pipeline.writePipe(pipes, i, cmd_count, &cmd_io) else cmd_io.stdout; - cmd_io.stdin = stdin; - cmd_io.stdout = stdout; - const subshell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, cmd_io, .pipeline)) { - .result => |s| s, - .err => |err| { - const system_err = err.toSystemError(); - this.writeFailingError("bun: {s}\n", .{system_err.message}, 1); - return .yield; - }, - }; - this.cmds.?[i] = .{ .cmd = Cmd.init(this.base.interpreter, subshell_state, item.cmd, Cmd.ParentPtr.init(this), cmd_io) }; - i += 1; - }, - // in a pipeline assignments have no effect - .assigns => {}, - .subshell => @panic(SUBSHELL_TODO_ERROR), - } + var i: u32 = 0; + const evtloop = this.base.eventLoop(); + for (this.node.items) |*item| { + switch (item.*) { + .cmd => { + const kind = "subproc"; + _ = kind; + var cmd_io = this.getIO(); + const stdin = if (cmd_count > 1) Pipeline.readPipe(pipes, i, &cmd_io, evtloop) else cmd_io.stdin.ref(); + const stdout = if (cmd_count > 1) Pipeline.writePipe(pipes, i, cmd_count, &cmd_io, evtloop) else cmd_io.stdout.ref(); + cmd_io.stdin = stdin; + cmd_io.stdout = stdout; + _ = cmd_io.stderr.ref(); + const subshell_state = switch (this.base.shell.dupeForSubshell(this.base.interpreter.allocator, cmd_io, .pipeline)) { + .result => |s| s, + .err => |err| { + const system_err = err.toSystemError(); + this.writeFailingError("bun: {s}\n", .{system_err.message}); + return .yield; + }, + }; + this.cmds.?[i] = .{ .cmd = Cmd.init(this.base.interpreter, subshell_state, item.cmd, Cmd.ParentPtr.init(this), cmd_io) }; + i += 1; + }, + // in a pipeline assignments have no effect + .assigns => {}, + .subshell => @panic(SUBSHELL_TODO_ERROR), } + } - this.pipes = pipes; + this.pipes = pipes; - return .cont; - } + return .cont; + } - pub fn writeFailingError(this: *Pipeline, comptime fmt: []const u8, args: anytype, exit_code: ExitCode) void { - _ = exit_code; // autofix + pub fn start(this: *Pipeline) void { + if (this.setupCommands() == .yield) return; - const HandleIOWrite = struct { - fn run(pipeline: *Pipeline, bufw: BufferedWriter) void { - pipeline.state = .{ .waiting_write_err = bufw }; - pipeline.state.waiting_write_err.writeIfPossible(false); - } - }; + if (this.state == .waiting_write_err or this.state == .done) return; + const cmds = this.cmds orelse { + this.state = .done; + this.parent.childDone(this, 0); + return; + }; - const buf = std.fmt.allocPrint(this.base.interpreter.arena.allocator(), fmt, args) catch bun.outOfMemory(); - _ = this.base.shell.writeFailingError(buf, this, HandleIOWrite.run); + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.exited_count == 0); + } + log("pipeline start {x} (count={d})", .{ @intFromPtr(this), this.node.items.len }); + if (this.node.items.len == 0) { + this.state = .done; + this.parent.childDone(this, 0); + return; } - pub fn start(this: *Pipeline) void { - if (this.setupCommands() == .yield) return; - - if (this.state == .waiting_write_err or this.state == .done) return; - const cmds = this.cmds orelse { - this.state = .done; - this.parent.childDone(this, 0); - return; - }; + for (cmds) |*cmd_or_result| { + // var stdin: IO.InKind = if (i == 0) this.getIO().stdin.ref() else .{ .fd = CowFd.init(this.pipes.?[i - 1][0]) }; + // var stdout: IO.OutKind = brk: { + // if (i == cmds.len - 1) break :brk this.getIO().stdout.ref(); - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.exited_count == 0); - } - log("pipeline start {x} (count={d})", .{ @intFromPtr(this), this.node.items.len }); - if (this.node.items.len == 0) { - this.state = .done; - this.parent.childDone(this, 0); - return; - } + // const fd = this.pipes.?[i][1]; + // const writer = IOWriter.init(fd, this.base.eventLoop()); + // break :brk .{ .fd = .{ .writer = writer } }; + // }; - for (cmds, 0..) |*cmd_or_result, i| { - var stdin: IO.Kind = if (i == 0) this.getIO().stdin else .{ .fd = this.pipes.?[i - 1][0] }; - var stdout: IO.Kind = if (i == cmds.len - 1) this.getIO().stdout else .{ .fd = this.pipes.?[i][1] }; + std.debug.assert(cmd_or_result.* == .cmd); + var cmd = cmd_or_result.cmd; + // var stdin = cmd.io.stdin; + // var stdout = cmd.io.stdout; + // const is_subproc = cmd.isSubproc(); + cmd.start(); - std.debug.assert(cmd_or_result.* == .cmd); - var cmd = cmd_or_result.cmd; - log("Spawn: proc_idx={d} stdin={any} stdout={any} stderr={any}\n", .{ i, stdin, stdout, cmd.io.stderr }); - cmd.start(); + // If command is a subproc (and not a builtin) we need to close the fd + // if (cmd.isSubproc()) { + // stdin.close(); + // stdout.close(); + // } + } + } - // If command is a subproc (and not a builtin) we need to close the fd - if (cmd.isSubproc()) { - stdin.close(); - stdout.close(); - } - } + pub fn onIOWriterChunk(this: *Pipeline, err: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .waiting_write_err); } - pub fn onBufferedWriterDone(this: *Pipeline, err: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .waiting_write_err); - } + if (err) |e| { + this.base.throw(&shell.ShellErr.newSys(e)); + return; + } - if (err) |e| { - global_handle.get().actuallyThrow(shell.ShellErr.newSys(e)); - return; - } + this.state = .done; + this.parent.childDone(this, 0); + } - this.state = .done; - this.parent.childDone(this, 0); + pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.cmds.?.len > 0); } - pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.cmds.?.len > 0); + const idx = brk: { + const ptr_value: u64 = @bitCast(child.ptr.repr); + _ = ptr_value; + for (this.cmds.?, 0..) |cmd_or_result, i| { + if (cmd_or_result == .cmd) { + if (@intFromPtr(cmd_or_result.cmd) == @as(usize, @intCast(child.ptr.repr._ptr))) break :brk i; + } } + unreachable; + }; - const idx = brk: { - const ptr_value: u64 = @bitCast(child.ptr.repr); - _ = ptr_value; - for (this.cmds.?, 0..) |cmd_or_result, i| { - if (cmd_or_result == .cmd) { - if (@intFromPtr(cmd_or_result.cmd) == @as(usize, @intCast(child.ptr.repr._ptr))) break :brk i; - } - } - unreachable; - }; + log("pipeline child done {x} ({d}) i={d}", .{ @intFromPtr(this), exit_code, idx }); + if (child.ptr.is(Cmd)) { + const cmd = child.as(Cmd); + cmd.base.shell.deinit(); + } - log("pipeline child done {x} ({d}) i={d}", .{ @intFromPtr(this), exit_code, idx }); - if (child.ptr.is(Cmd)) { - const cmd = child.as(Cmd); - cmd.base.shell.deinit(); - } + child.deinit(); + this.cmds.?[idx] = .{ .result = exit_code }; + this.exited_count += 1; - child.deinit(); - this.cmds.?[idx] = .{ .result = exit_code }; - this.exited_count += 1; - - if (this.exited_count >= this.cmds.?.len) { - var last_exit_code: ExitCode = 0; - for (this.cmds.?) |cmd_or_result| { - if (cmd_or_result == .result) { - last_exit_code = cmd_or_result.result; - break; - } + if (this.exited_count >= this.cmds.?.len) { + var last_exit_code: ExitCode = 0; + for (this.cmds.?) |cmd_or_result| { + if (cmd_or_result == .result) { + last_exit_code = cmd_or_result.result; + break; } - this.state = .done; - this.parent.childDone(this, last_exit_code); - return; } + this.state = .done; + this.parent.childDone(this, last_exit_code); + return; } + } - pub fn deinit(this: *Pipeline) void { - // If commands was zero then we didn't allocate anything - if (this.cmds == null) return; - for (this.cmds.?) |*cmd_or_result| { - if (cmd_or_result.* == .cmd) { - cmd_or_result.cmd.deinit(); - } + pub fn deinit(this: *Pipeline) void { + // If commands was zero then we didn't allocate anything + if (this.cmds == null) return; + for (this.cmds.?) |*cmd_or_result| { + if (cmd_or_result.* == .cmd) { + cmd_or_result.cmd.deinit(); } - if (this.pipes) |pipes| { - this.base.interpreter.allocator.free(pipes); - } - if (this.cmds) |cmds| { - this.base.interpreter.allocator.free(cmds); - } - this.base.interpreter.allocator.destroy(this); } + if (this.pipes) |pipes| { + this.base.interpreter.allocator.free(pipes); + } + if (this.cmds) |cmds| { + this.base.interpreter.allocator.free(cmds); + } + this.io.deref(); + this.base.interpreter.allocator.destroy(this); + } - fn initializePipes(pipes: []Pipe, set_count: *u32) Maybe(void) { - for (pipes) |*pipe| { + fn initializePipes(pipes: []Pipe, set_count: *u32) Maybe(void) { + for (pipes) |*pipe| { + if (bun.Environment.isWindows) { + var fds: [2]uv.uv_file = undefined; + if (uv.uv_pipe(&fds, 0, 0).errEnum()) |e| { + return .{ .err = Syscall.Error.fromCode(e, .pipe) }; + } + pipe[0] = bun.FDImpl.fromUV(fds[0]).encode(); + pipe[1] = bun.FDImpl.fromUV(fds[1]).encode(); + } else { pipe.* = switch (Syscall.pipe()) { .err => |e| return .{ .err = e }, .result => |p| p, }; - set_count.* += 1; } - return Maybe(void).success; - } - - fn writePipe(pipes: []Pipe, proc_idx: usize, cmd_count: usize, io: *IO) IO.Kind { - // Last command in the pipeline should write to stdout - if (proc_idx == cmd_count - 1) return io.stdout; - return .{ .fd = pipes[proc_idx][1] }; - } - - fn readPipe(pipes: []Pipe, proc_idx: usize, io: *IO) IO.Kind { - // First command in the pipeline should read from stdin - if (proc_idx == 0) return io.stdin; - return .{ .fd = pipes[proc_idx - 1][0] }; + set_count.* += 1; } - }; + return Maybe(void).success; + } - pub const Cmd = struct { - base: State, - node: *const ast.Cmd, - parent: ParentPtr, + fn writePipe(pipes: []Pipe, proc_idx: usize, cmd_count: usize, io: *IO, evtloop: JSC.EventLoopHandle) IO.OutKind { + // Last command in the pipeline should write to stdout + if (proc_idx == cmd_count - 1) return io.stdout.ref(); + return .{ .fd = .{ .writer = IOWriter.init(pipes[proc_idx][1], evtloop) } }; + } - /// Arena used for memory needed to spawn command. - /// For subprocesses: - /// - allocates argv, env array, etc. - /// - Freed after calling posix spawn since its not needed anymore - /// For Builtins: - /// - allocates argv, sometimes used by the builtin for small allocations. - /// - Freed when builtin is done (since it contains argv which might be used at any point) - spawn_arena: bun.ArenaAllocator, - spawn_arena_freed: bool = false, - - /// This allocated by the above arena - args: std.ArrayList(?[*:0]const u8), - - /// If the cmd redirects to a file we have to expand that string. - /// Allocated in `spawn_arena` - redirection_file: std.ArrayList(u8), - redirection_fd: bun.FileDescriptor = bun.invalid_fd, - - exec: Exec = .none, - exit_code: ?ExitCode = null, - io: IO, - // duplicate_out: enum { none, stdout, stderr } = .none, - freed: bool = false, + fn readPipe(pipes: []Pipe, proc_idx: usize, io: *IO, evtloop: JSC.EventLoopHandle) IO.InKind { + // First command in the pipeline should read from stdin + if (proc_idx == 0) return io.stdin.ref(); + return .{ .fd = IOReader.init(pipes[proc_idx - 1][0], evtloop) }; + } + }; - state: union(enum) { - idle, - expanding_assigns: Assigns, - expanding_redirect: struct { - idx: u32 = 0, - expansion: Expansion, - }, - expanding_args: struct { - idx: u32 = 0, - expansion: Expansion, - }, - exec, - done, - waiting_write_err: BufferedWriter, - err: ?Syscall.Error, + pub const Cmd = struct { + base: State, + node: *const ast.Cmd, + parent: ParentPtr, + + /// Arena used for memory needed to spawn command. + /// For subprocesses: + /// - allocates argv, env array, etc. + /// - Freed after calling posix spawn since its not needed anymore + /// For Builtins: + /// - allocates argv, sometimes used by the builtin for small allocations. + /// - Freed when builtin is done (since it contains argv which might be used at any point) + spawn_arena: bun.ArenaAllocator, + spawn_arena_freed: bool = false, + + /// This allocated by the above arena + args: std.ArrayList(?[*:0]const u8), + + /// If the cmd redirects to a file we have to expand that string. + /// Allocated in `spawn_arena` + redirection_file: std.ArrayList(u8), + redirection_fd: ?*CowFd = null, + + exec: Exec = .none, + exit_code: ?ExitCode = null, + io: IO, + freed: bool = false, + + state: union(enum) { + idle, + expanding_assigns: Assigns, + expanding_redirect: struct { + idx: u32 = 0, + expansion: Expansion, }, + expanding_args: struct { + idx: u32 = 0, + expansion: Expansion, + }, + exec, + done, + waiting_write_err, + }, + + const Subprocess = bun.shell.subproc.ShellSubprocess; + + pub const Exec = union(enum) { + none, + bltn: Builtin, + subproc: struct { + child: *Subprocess, + buffered_closed: BufferedIoClosed = .{}, + }, + }; - const Subprocess = bun.shell.subproc.NewShellSubprocess(EventLoopKind, @This()); - - pub const Exec = union(enum) { - none, - bltn: Builtin, - subproc: struct { - child: *Subprocess, - buffered_closed: BufferedIoClosed = .{}, - }, - }; - - const BufferedIoClosed = struct { - stdin: ?bool = null, - stdout: ?BufferedIoState = null, - stderr: ?BufferedIoState = null, + const BufferedIoClosed = struct { + stdin: ?bool = null, + stdout: ?BufferedIoState = null, + stderr: ?BufferedIoState = null, - const BufferedIoState = struct { - state: union(enum) { - open, - closed: bun.ByteList, - } = .open, - owned: bool = false, - - /// BufferedInput/Output uses jsc vm allocator - pub fn deinit(this: *BufferedIoState, jsc_vm_allocator: Allocator) void { - if (this.state == .closed and this.owned) { - var list = bun.ByteList.listManaged(this.state.closed, jsc_vm_allocator); - list.deinit(); - this.state.closed = .{}; - } - } + const BufferedIoState = struct { + state: union(enum) { + open, + closed: bun.ByteList, + } = .open, + owned: bool = false, - pub fn closed(this: *BufferedIoState) bool { - return this.state == .closed; + /// BufferedInput/Output uses jsc vm allocator + pub fn deinit(this: *BufferedIoState, jsc_vm_allocator: Allocator) void { + if (this.state == .closed and this.owned) { + var list = bun.ByteList.listManaged(this.state.closed, jsc_vm_allocator); + list.deinit(); + this.state.closed = .{}; } - }; + } - fn deinit(this: *BufferedIoClosed, jsc_vm_allocator: Allocator) void { - if (this.stdin) |*io| { - _ = io; // autofix + pub fn closed(this: *BufferedIoState) bool { + return this.state == .closed; + } + }; - // io.deinit(jsc_vm_allocator); - } + fn deinit(this: *BufferedIoClosed, jsc_vm_allocator: Allocator) void { + if (this.stdin) |*io| { + _ = io; // autofix - if (this.stdout) |*io| { - io.deinit(jsc_vm_allocator); - } + // io.deinit(jsc_vm_allocator); + } - if (this.stderr) |*io| { - io.deinit(jsc_vm_allocator); - } + if (this.stdout) |*io| { + io.deinit(jsc_vm_allocator); } - fn allClosed(this: *BufferedIoClosed) bool { - return (if (this.stdin) |stdin| stdin else true) and - (if (this.stdout) |*stdout| stdout.closed() else true) and - (if (this.stderr) |*stderr| stderr.closed() else true); + if (this.stderr) |*io| { + io.deinit(jsc_vm_allocator); } + } - fn close(this: *BufferedIoClosed, cmd: *Cmd, io: union(enum) { stdout: *Subprocess.Readable, stderr: *Subprocess.Readable, stdin }) void { - switch (io) { - .stdout => { - if (this.stdout) |*stdout| { - const readable = io.stdout; + fn allClosed(this: *BufferedIoClosed) bool { + const ret = (if (this.stdin) |stdin| stdin else true) and + (if (this.stdout) |*stdout| stdout.closed() else true) and + (if (this.stderr) |*stderr| stderr.closed() else true); + log("BufferedIOClosed(0x{x}) all_closed={any} stdin={any} stdout={any} stderr={any}", .{ @intFromPtr(this), ret, if (this.stdin) |stdin| stdin else true, if (this.stdout) |*stdout| stdout.closed() else true, if (this.stderr) |*stderr| stderr.closed() else true }); + return ret; + } - // If the shell state is piped (inside a cmd substitution) aggregate the output of this command - if (cmd.base.shell.io.stdout == .pipe and cmd.io.stdout == .pipe and !cmd.node.redirect.redirectsElsewhere(.stdout)) { - cmd.base.shell.buffered_stdout().append(bun.default_allocator, readable.pipe.buffer.internal_buffer.slice()) catch bun.outOfMemory(); - } + fn close(this: *BufferedIoClosed, cmd: *Cmd, io: union(enum) { stdout: *Subprocess.Readable, stderr: *Subprocess.Readable, stdin }) void { + switch (io) { + .stdout => { + if (this.stdout) |*stdout| { + const readable = io.stdout; - stdout.state = .{ .closed = readable.pipe.buffer.internal_buffer }; - io.stdout.pipe.buffer.internal_buffer = .{}; + // If the shell state is piped (inside a cmd substitution) aggregate the output of this command + if (cmd.io.stdout == .pipe and cmd.io.stdout == .pipe and !cmd.node.redirect.redirectsElsewhere(.stdout)) { + const the_slice = readable.pipe.slice(); + cmd.base.shell.buffered_stdout().append(bun.default_allocator, the_slice) catch bun.outOfMemory(); } - }, - .stderr => { - if (this.stderr) |*stderr| { - const readable = io.stderr; - - // If the shell state is piped (inside a cmd substitution) aggregate the output of this command - if (cmd.base.shell.io.stderr == .pipe and cmd.io.stderr == .pipe and !cmd.node.redirect.redirectsElsewhere(.stderr)) { - cmd.base.shell.buffered_stderr().append(bun.default_allocator, readable.pipe.buffer.internal_buffer.slice()) catch bun.outOfMemory(); - } - stderr.state = .{ .closed = readable.pipe.buffer.internal_buffer }; - io.stderr.pipe.buffer.internal_buffer = .{}; + stdout.state = .{ .closed = bun.ByteList.fromList(readable.pipe.takeBuffer()) }; + } + }, + .stderr => { + if (this.stderr) |*stderr| { + const readable = io.stderr; + + // If the shell state is piped (inside a cmd substitution) aggregate the output of this command + if (cmd.io.stderr == .pipe and cmd.io.stderr == .pipe and !cmd.node.redirect.redirectsElsewhere(.stderr)) { + const the_slice = readable.pipe.slice(); + cmd.base.shell.buffered_stderr().append(bun.default_allocator, the_slice) catch bun.outOfMemory(); } - }, - .stdin => { - this.stdin = true; - // if (this.stdin) |*stdin| { - // stdin.state = .{ .closed = .{} }; - // } - }, - } - } - - fn isBuffered(this: *BufferedIoClosed, comptime io: enum { stdout, stderr, stdin }) bool { - return @field(this, @tagName(io)) != null; - } - fn fromStdio(io: *const [3]bun.shell.subproc.Stdio) BufferedIoClosed { - return .{ - .stdin = if (io[stdin_no].isPiped()) false else null, - .stdout = if (io[stdout_no].isPiped()) .{ .owned = io[stdout_no] == .pipe } else null, - .stderr = if (io[stderr_no].isPiped()) .{ .owned = io[stderr_no] == .pipe } else null, - }; + stderr.state = .{ .closed = bun.ByteList.fromList(readable.pipe.takeBuffer()) }; + // io.stderr.pipe.buffer.internal_buffer = .{}; + } + }, + .stdin => { + this.stdin = true; + // if (this.stdin) |*stdin| { + // stdin.state = .{ .closed = .{} }; + // } + }, } - }; - - const ParentPtr = StatePtrUnion(.{ - Stmt, - Cond, - Pipeline, - // Expansion, - // TODO - // .subst = void, - }); - - const ChildPtr = StatePtrUnion(.{ - Assigns, - Expansion, - }); - - pub fn isSubproc(this: *Cmd) bool { - return this.exec == .subproc; } - /// If starting a command results in an error (failed to find executable in path for example) - /// then it should write to the stderr of the entire shell script process - pub fn writeFailingError(this: *Cmd, buf: []const u8, exit_code: ExitCode) void { - _ = exit_code; // autofix + fn isBuffered(this: *BufferedIoClosed, comptime io: enum { stdout, stderr, stdin }) bool { + return @field(this, @tagName(io)) != null; + } - const HandleIOWrite = struct { - fn run(cmd: *Cmd, bufw: BufferedWriter) void { - cmd.state = .{ .waiting_write_err = bufw }; - cmd.state.waiting_write_err.writeIfPossible(false); - } + fn fromStdio(io: *const [3]bun.shell.subproc.Stdio) BufferedIoClosed { + return .{ + .stdin = if (io[stdin_no].isPiped()) false else null, + .stdout = if (io[stdout_no].isPiped()) .{ .owned = io[stdout_no] == .pipe } else null, + .stderr = if (io[stderr_no].isPiped()) .{ .owned = io[stderr_no] == .pipe } else null, }; - _ = this.base.shell.writeFailingError(buf, this, HandleIOWrite.run); - - // switch (this.base.shell.io.stderr) { - // .std => |val| { - // this.state = .{ .waiting_write_err = BufferedWriter{ - // .fd = stderr_no, - // .remain = buf, - // .parent = BufferedWriter.ParentPtr.init(this), - // .bytelist = val.captured, - // } }; - // this.state.waiting_write_err.writeIfPossible(false); - // }, - // .fd => { - // this.state = .{ .waiting_write_err = BufferedWriter{ - // .fd = stderr_no, - // .remain = buf, - // .parent = BufferedWriter.ParentPtr.init(this), - // } }; - // this.state.waiting_write_err.writeIfPossible(false); - // }, - // .pipe, .ignore => { - // this.parent.childDone(this, 1); - // }, - // } - return; } + }; - pub fn init( - interpreter: *ThisInterpreter, - shell_state: *ShellState, - node: *const ast.Cmd, - parent: ParentPtr, - io: IO, - ) *Cmd { - var cmd = interpreter.allocator.create(Cmd) catch |err| { - std.debug.print("Ruh roh: {any}\n", .{err}); - @panic("Ruh roh"); - }; - cmd.* = .{ - .base = .{ .kind = .cmd, .interpreter = interpreter, .shell = shell_state }, - .node = node, - .parent = parent, + const ParentPtr = StatePtrUnion(.{ + Stmt, + Cond, + Pipeline, + // Expansion, + // TODO + // .subst = void, + }); + + const ChildPtr = StatePtrUnion(.{ + Assigns, + Expansion, + }); - .spawn_arena = bun.ArenaAllocator.init(interpreter.allocator), - .args = std.ArrayList(?[*:0]const u8).initCapacity(cmd.spawn_arena.allocator(), node.name_and_args.len) catch bun.outOfMemory(), - .redirection_file = undefined, + pub fn isSubproc(this: *Cmd) bool { + return this.exec == .subproc; + } - .exit_code = null, - .io = io, - .state = .idle, - }; + /// If starting a command results in an error (failed to find executable in path for example) + /// then it should write to the stderr of the entire shell script process + pub fn writeFailingError(this: *Cmd, comptime fmt: []const u8, args: anytype) void { + const handler = struct { + fn enqueueCb(ctx: *Cmd) void { + ctx.state = .waiting_write_err; + } + }; + this.base.shell.writeFailingErrorFmt(this, handler.enqueueCb, fmt, args); + } - cmd.redirection_file = std.ArrayList(u8).init(cmd.spawn_arena.allocator()); + pub fn init( + interpreter: *ThisInterpreter, + shell_state: *ShellState, + node: *const ast.Cmd, + parent: ParentPtr, + io: IO, + ) *Cmd { + var cmd = interpreter.allocator.create(Cmd) catch |err| { + std.debug.print("Ruh roh: {any}\n", .{err}); + @panic("Ruh roh"); + }; + cmd.* = .{ + .base = .{ .kind = .cmd, .interpreter = interpreter, .shell = shell_state }, + .node = node, + .parent = parent, + + .spawn_arena = bun.ArenaAllocator.init(interpreter.allocator), + .args = std.ArrayList(?[*:0]const u8).initCapacity(cmd.spawn_arena.allocator(), node.name_and_args.len) catch bun.outOfMemory(), + .redirection_file = undefined, + + .exit_code = null, + .io = io, + .state = .idle, + }; - return cmd; - } + cmd.redirection_file = std.ArrayList(u8).init(cmd.spawn_arena.allocator()); - pub fn next(this: *Cmd) void { - while (!(this.state == .done or this.state == .err)) { - switch (this.state) { - .idle => { - this.state = .{ .expanding_assigns = undefined }; - Assigns.init(&this.state.expanding_assigns, this.base.interpreter, this.base.shell, this.node.assigns, .cmd, Assigns.ParentPtr.init(this)); - this.state.expanding_assigns.start(); - return; // yield execution - }, - .expanding_assigns => { - return; // yield execution - }, - .expanding_redirect => { - if (this.state.expanding_redirect.idx >= 1) { - this.state = .{ - .expanding_args = undefined, - }; - continue; - } - this.state.expanding_redirect.idx += 1; - - // Get the node to expand otherwise go straight to - // `expanding_args` state - const node_to_expand = brk: { - if (this.node.redirect_file != null and this.node.redirect_file.? == .atom) break :brk &this.node.redirect_file.?.atom; - this.state = .{ - .expanding_args = .{ - .expansion = undefined, - }, - }; - continue; + return cmd; + } + + pub fn next(this: *Cmd) void { + while (this.state != .done) { + switch (this.state) { + .idle => { + this.state = .{ .expanding_assigns = undefined }; + Assigns.init(&this.state.expanding_assigns, this.base.interpreter, this.base.shell, this.node.assigns, .cmd, Assigns.ParentPtr.init(this), this.io.copy()); + this.state.expanding_assigns.start(); + return; // yield execution + }, + .expanding_assigns => { + return; // yield execution + }, + .expanding_redirect => { + if (this.state.expanding_redirect.idx >= 1) { + this.state = .{ + .expanding_args = undefined, }; + continue; + } + this.state.expanding_redirect.idx += 1; - this.redirection_file = std.ArrayList(u8).init(this.spawn_arena.allocator()); - - Expansion.init( - this.base.interpreter, - this.base.shell, - &this.state.expanding_redirect.expansion, - node_to_expand, - Expansion.ParentPtr.init(this), - .{ - .single = .{ - .list = &this.redirection_file, - }, + // Get the node to expand otherwise go straight to + // `expanding_args` state + const node_to_expand = brk: { + if (this.node.redirect_file != null and this.node.redirect_file.? == .atom) break :brk &this.node.redirect_file.?.atom; + this.state = .{ + .expanding_args = .{ + .expansion = undefined, }, - ); + }; + continue; + }; - this.state.expanding_redirect.expansion.start(); - return; - }, - .expanding_args => { - if (this.state.expanding_args.idx >= this.node.name_and_args.len) { - this.transitionToExecStateAndYield(); - // yield execution to subproc - return; - } + this.redirection_file = std.ArrayList(u8).init(this.spawn_arena.allocator()); - this.args.ensureUnusedCapacity(1) catch bun.outOfMemory(); - Expansion.init( - this.base.interpreter, - this.base.shell, - &this.state.expanding_args.expansion, - &this.node.name_and_args[this.state.expanding_args.idx], - Expansion.ParentPtr.init(this), - .{ - .array_of_ptr = &this.args, + Expansion.init( + this.base.interpreter, + this.base.shell, + &this.state.expanding_redirect.expansion, + node_to_expand, + Expansion.ParentPtr.init(this), + .{ + .single = .{ + .list = &this.redirection_file, }, - ); - - this.state.expanding_args.idx += 1; + }, + this.io.copy(), + ); - this.state.expanding_args.expansion.start(); - // yield execution to expansion - return; - }, - .waiting_write_err => { - return; - }, - .exec => { - // yield execution to subproc/builtin + this.state.expanding_redirect.expansion.start(); + return; + }, + .expanding_args => { + if (this.state.expanding_args.idx >= this.node.name_and_args.len) { + this.transitionToExecStateAndYield(); + // yield execution to subproc return; - }, - .done, .err => unreachable, - } - } + } - if (this.state == .done) { - this.parent.childDone(this, this.exit_code.?); - return; + this.args.ensureUnusedCapacity(1) catch bun.outOfMemory(); + Expansion.init( + this.base.interpreter, + this.base.shell, + &this.state.expanding_args.expansion, + &this.node.name_and_args[this.state.expanding_args.idx], + Expansion.ParentPtr.init(this), + .{ + .array_of_ptr = &this.args, + }, + this.io.copy(), + ); + + this.state.expanding_args.idx += 1; + + this.state.expanding_args.expansion.start(); + // yield execution to expansion + return; + }, + .waiting_write_err => { + return; + }, + .exec => { + // yield execution to subproc/builtin + return; + }, + .done => unreachable, } + } - this.parent.childDone(this, 1); + if (this.state == .done) { + this.parent.childDone(this, this.exit_code.?); return; } - fn transitionToExecStateAndYield(this: *Cmd) void { - this.state = .exec; - this.initSubproc(); + this.parent.childDone(this, 1); + return; + } + + fn transitionToExecStateAndYield(this: *Cmd) void { + this.state = .exec; + this.initSubproc(); + } + + pub fn start(this: *Cmd) void { + log("cmd start {x}", .{@intFromPtr(this)}); + return this.next(); + } + + pub fn onIOWriterChunk(this: *Cmd, e: ?JSC.SystemError) void { + if (e) |err| { + this.base.throw(&bun.shell.ShellErr.newSys(err)); + return; } + std.debug.assert(this.state == .waiting_write_err); + this.parent.childDone(this, 1); + return; + } - pub fn start(this: *Cmd) void { - log("cmd start {x}", .{@intFromPtr(this)}); - return this.next(); + pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { + if (child.ptr.is(Assigns)) { + if (exit_code != 0) { + const err = this.state.expanding_assigns.state.err; + defer err.deinit(bun.default_allocator); + this.state.expanding_assigns.deinit(); + const buf = err.fmt(); + this.writeFailingError("{s}", .{buf}); + return; + } + + this.state.expanding_assigns.deinit(); + this.state = .{ + .expanding_redirect = .{ + .expansion = undefined, + }, + }; + this.next(); + return; } - pub fn onBufferedWriterDone(this: *Cmd, e: ?Syscall.Error) void { - if (e) |err| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(err)); + if (child.ptr.is(Expansion)) { + child.deinit(); + if (exit_code != 0) { + const err = switch (this.state) { + .expanding_redirect => this.state.expanding_redirect.expansion.state.err, + .expanding_args => this.state.expanding_args.expansion.state.err, + else => @panic("Invalid state"), + }; + defer err.deinit(bun.default_allocator); + const buf = err.fmt(); + this.writeFailingError("{s}", .{buf}); return; } - std.debug.assert(this.state == .waiting_write_err); - this.state = .{ .err = e }; this.next(); return; } + unreachable; + } - pub fn childDone(this: *Cmd, child: ChildPtr, exit_code: ExitCode) void { - if (child.ptr.is(Assigns)) { - if (exit_code != 0) { - const err = this.state.expanding_assigns.state.err; - defer err.deinit(bun.default_allocator); - this.state.expanding_assigns.deinit(); - const buf = err.fmt(); - this.writeFailingError(buf, exit_code); - return; - } - - this.state.expanding_assigns.deinit(); - this.state = .{ - .expanding_redirect = .{ - .expansion = undefined, - }, - }; - this.next(); - return; - } - - if (child.ptr.is(Expansion)) { - child.deinit(); - if (exit_code != 0) { - const err = switch (this.state) { - .expanding_redirect => this.state.expanding_redirect.expansion.state.err, - .expanding_args => this.state.expanding_args.expansion.state.err, - else => |t| std.debug.panic("Unexpected state .{s} in Bun shell", .{@tagName(t)}), - }; - defer err.deinit(bun.default_allocator); - const buf = err.fmt(); - this.writeFailingError(buf, exit_code); - return; - } - this.next(); - return; - } - unreachable; - } + fn initSubproc(this: *Cmd) void { + log("cmd init subproc ({x}, cwd={s})", .{ @intFromPtr(this), this.base.shell.cwd() }); - fn initSubproc(this: *Cmd) void { - log("cmd init subproc ({x}, cwd={s})", .{ @intFromPtr(this), this.base.shell.cwd() }); + var arena = &this.spawn_arena; + var arena_allocator = arena.allocator(); - var arena = &this.spawn_arena; - var arena_allocator = arena.allocator(); - - // for (this.node.assigns) |*assign| { - // this.base.interpreter.assignVar(assign, .cmd); - // } + // for (this.node.assigns) |*assign| { + // this.base.interpreter.assignVar(assign, .cmd); + // } - var spawn_args = Subprocess.SpawnArgs.default(arena, this.base.interpreter.global, false); + var spawn_args = Subprocess.SpawnArgs.default(arena, this.base.interpreter.event_loop, false); - spawn_args.argv = std.ArrayListUnmanaged(?[*:0]const u8){}; - spawn_args.cmd_parent = this; - spawn_args.cwd = this.base.shell.cwdZ(); + spawn_args.argv = std.ArrayListUnmanaged(?[*:0]const u8){}; + spawn_args.cmd_parent = this; + spawn_args.cwd = this.base.shell.cwdZ(); - const args = args: { - this.args.append(null) catch bun.outOfMemory(); + const args = args: { + this.args.append(null) catch bun.outOfMemory(); - if (bun.Environment.allow_assert) { - for (this.args.items) |maybe_arg| { - if (maybe_arg) |arg| { - log("ARG: {s}\n", .{arg}); - } + if (bun.Environment.allow_assert) { + for (this.args.items) |maybe_arg| { + if (maybe_arg) |arg| { + log("ARG: {s}\n", .{arg}); } } + } - const first_arg = this.args.items[0] orelse { - // If no args then this is a bug - @panic("No arguments provided"); - }; + const first_arg = this.args.items[0] orelse { + // If no args then this is a bug + @panic("No arguments provided"); + }; - const first_arg_len = std.mem.len(first_arg); - - if (Builtin.Kind.fromStr(first_arg[0..first_arg_len])) |b| { - // const cwd = switch (Syscall.dup(this.base.shell.cwd_fd)) { - // .err => |e| { - // var buf = std.ArrayList(u8).init(arena_allocator); - // const writer = buf.writer(); - // e.format("bun: ", .{}, writer) catch bun.outOfMemory(); - // this.writeFailingError(buf.items[0..], e.errno); - // return; - // }, - // .result => |fd| fd, - // }; - const cwd = this.base.shell.cwd_fd; - const coro_result = Builtin.init( - this, - this.base.interpreter, - b, - arena, - this.node, - &this.args, - &this.base.shell.export_env, - &this.base.shell.cmd_local_env, - // this.base.shell.export_env.cloneWithAllocator(arena_allocator), - // this.base.shell.cmd_local_env.cloneWithAllocator(arena_allocator), - cwd, - &this.io, - false, - ); - if (coro_result == .yield) return; + const first_arg_len = std.mem.len(first_arg); + + if (Builtin.Kind.fromStr(first_arg[0..first_arg_len])) |b| { + // const cwd = switch (Syscall.dup(this.base.shell.cwd_fd)) { + // .err => |e| { + // var buf = std.ArrayList(u8).init(arena_allocator); + // const writer = buf.writer(); + // e.format("bun: ", .{}, writer) catch bun.outOfMemory(); + // this.writeFailingError(buf.items[0..], e.errno); + // return; + // }, + // .result => |fd| fd, + // }; + const cwd = this.base.shell.cwd_fd; + const coro_result = Builtin.init( + this, + this.base.interpreter, + b, + arena, + this.node, + &this.args, + &this.base.shell.export_env, + &this.base.shell.cmd_local_env, + // this.base.shell.export_env.cloneWithAllocator(arena_allocator), + // this.base.shell.cmd_local_env.cloneWithAllocator(arena_allocator), + cwd, + &this.io, + false, + ); + if (coro_result == .yield) return; - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.exec == .bltn); - } + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.exec == .bltn); + } - log("WTF: {s}", .{@tagName(this.exec)}); + log("WTF: {s}", .{@tagName(this.exec)}); - switch (this.exec.bltn.start()) { - .result => {}, - .err => |e| { - const buf = std.fmt.allocPrint(this.spawn_arena.allocator(), "bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toSystemError().message }) catch bun.outOfMemory(); - this.writeFailingError(buf, 1); - return; - }, - } - return; + switch (this.exec.bltn.start()) { + .result => {}, + .err => |e| { + this.writeFailingError("bun: {s}: {s}", .{ @tagName(this.exec.bltn.kind), e.toSystemError().message }); + return; + }, } + return; + } - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const resolved = which(&path_buf, spawn_args.PATH, spawn_args.cwd, first_arg[0..first_arg_len]) orelse { - const buf = std.fmt.allocPrint(arena_allocator, "bun: command not found: {s}\n", .{first_arg}) catch bun.outOfMemory(); - this.writeFailingError(buf, 1); - return; - }; + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const resolved = which(&path_buf, spawn_args.PATH, spawn_args.cwd, first_arg[0..first_arg_len]) orelse { + this.writeFailingError("bun: command not found: {s}\n", .{first_arg}); + return; + }; - const duped = arena_allocator.dupeZ(u8, bun.span(resolved)) catch bun.outOfMemory(); - this.args.items[0] = duped; + const duped = arena_allocator.dupeZ(u8, bun.span(resolved)) catch bun.outOfMemory(); + this.args.items[0] = duped; - break :args this.args; - }; - spawn_args.argv = std.ArrayListUnmanaged(?[*:0]const u8){ .items = args.items, .capacity = args.capacity }; + break :args this.args; + }; + spawn_args.argv = std.ArrayListUnmanaged(?[*:0]const u8){ .items = args.items, .capacity = args.capacity }; - // Fill the env from the export end and cmd local env - { - var env_iter = this.base.shell.export_env.iterator(); - spawn_args.fillEnv(&env_iter, false); - env_iter = this.base.shell.cmd_local_env.iterator(); - spawn_args.fillEnv(&env_iter, false); - } + // Fill the env from the export end and cmd local env + { + var env_iter = this.base.shell.export_env.iterator(); + spawn_args.fillEnv(&env_iter, false); + env_iter = this.base.shell.cmd_local_env.iterator(); + spawn_args.fillEnv(&env_iter, false); + } - this.io.to_subproc_stdio(&spawn_args.stdio); + this.io.to_subproc_stdio(&spawn_args.stdio); - if (this.node.redirect_file) |redirect| { - const in_cmd_subst = false; + if (this.node.redirect_file) |redirect| { + const in_cmd_subst = false; - if (comptime in_cmd_subst) { - setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, .ignore); - } else switch (redirect) { - .jsbuf => |val| { - // JS values in here is probably a bug - if (comptime EventLoopKind != .js) @panic("JS values not allowed in this context"); + if (comptime in_cmd_subst) { + setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, .ignore); + } else switch (redirect) { + .jsbuf => |val| { + // JS values in here is probably a bug + if (this.base.eventLoop() != .js) @panic("JS values not allowed in this context"); + const global = this.base.eventLoop().js.global; - if (this.base.interpreter.jsobjs[val.idx].asArrayBuffer(this.base.interpreter.global)) |buf| { - const stdio: bun.shell.subproc.Stdio = .{ .array_buffer = .{ - .buf = JSC.ArrayBuffer.Strong{ - .array_buffer = buf, - .held = JSC.Strong.create(buf.value, this.base.interpreter.global), - }, - .from_jsc = true, - } }; - - setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, stdio); - } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Blob)) |blob| { - if (this.node.redirect.stdin) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, .{ - .Blob = blob.*, - }, stdin_no, &spawn_args.stdio)) { - return; - } - } - if (this.node.redirect.stdout) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, .{ - .Blob = blob.*, - }, stdout_no, &spawn_args.stdio)) { - return; - } + if (this.base.interpreter.jsobjs[val.idx].asArrayBuffer(global)) |buf| { + const stdio: bun.shell.subproc.Stdio = .{ .array_buffer = JSC.ArrayBuffer.Strong{ + .array_buffer = buf, + .held = JSC.Strong.create(buf.value, global), + } }; + + setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, stdio); + } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Blob)) |blob| { + if (this.node.redirect.stdin) { + if (!spawn_args.stdio[stdin_no].extractBlob(global, .{ + .Blob = blob.*, + }, stdin_no)) { + return; } - if (this.node.redirect.stderr) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, .{ - .Blob = blob.*, - }, stderr_no, &spawn_args.stdio)) { - return; - } + } + if (this.node.redirect.stdout) { + if (!spawn_args.stdio[stdin_no].extractBlob(global, .{ + .Blob = blob.*, + }, stdout_no)) { + return; } - } else if (JSC.WebCore.ReadableStream.fromJS(this.base.interpreter.jsobjs[val.idx], this.base.interpreter.global)) |rstream| { - const stdio: bun.shell.subproc.Stdio = .{ - .pipe = rstream, - }; - - setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, stdio); - } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Response)) |req| { - req.getBodyValue().toBlobIfPossible(); - if (this.node.redirect.stdin) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, req.getBodyValue().useAsAnyBlob(), stdin_no, &spawn_args.stdio)) { - return; - } + } + if (this.node.redirect.stderr) { + if (!spawn_args.stdio[stdin_no].extractBlob(global, .{ + .Blob = blob.*, + }, stderr_no)) { + return; } - if (this.node.redirect.stdout) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, req.getBodyValue().useAsAnyBlob(), stdout_no, &spawn_args.stdio)) { - return; - } + } + } else if (JSC.WebCore.ReadableStream.fromJS(this.base.interpreter.jsobjs[val.idx], global)) |rstream| { + _ = rstream; + @panic("TODO SHELL READABLE STREAM"); + // const stdio: bun.shell.subproc.Stdio = .{ + // .pipe = rstream, + // }; + + // setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, stdio); + } else if (this.base.interpreter.jsobjs[val.idx].as(JSC.WebCore.Response)) |req| { + req.getBodyValue().toBlobIfPossible(); + if (this.node.redirect.stdin) { + if (!spawn_args.stdio[stdin_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdin_no)) { + return; } - if (this.node.redirect.stderr) { - if (!Subprocess.extractStdioBlob(this.base.interpreter.global, req.getBodyValue().useAsAnyBlob(), stderr_no, &spawn_args.stdio)) { - return; - } + } + if (this.node.redirect.stdout) { + if (!spawn_args.stdio[stdout_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stdout_no)) { + return; } - } else { - const jsval = this.base.interpreter.jsobjs[val.idx]; - global_handle.get().globalThis.throw( - "Unknown JS value used in shell: {}", - .{jsval.fmtString(global_handle.get().globalThis)}, - ); - return; } - }, - .atom => { - if (this.redirection_file.items.len == 0) { - const buf = std.fmt.allocPrint(spawn_args.arena.allocator(), "bun: ambiguous redirect: at `{s}`\n", .{spawn_args.argv.items[0] orelse ""}) catch bun.outOfMemory(); - this.writeFailingError(buf, 1); - return; + if (this.node.redirect.stderr) { + if (!spawn_args.stdio[stderr_no].extractBlob(global, req.getBodyValue().useAsAnyBlob(), stderr_no)) { + return; + } } - const path = this.redirection_file.items[0..this.redirection_file.items.len -| 1 :0]; - log("EXPANDED REDIRECT: {s}\n", .{this.redirection_file.items[0..]}); - const perm = 0o666; - const flags = this.node.redirect.toFlags(); - const redirfd = switch (Syscall.openat(this.base.shell.cwd_fd, path, flags, perm)) { - .err => |e| { - const buf = std.fmt.allocPrint(this.spawn_arena.allocator(), "bun: {s}: {s}", .{ e.toSystemError().message, path }) catch bun.outOfMemory(); - return this.writeFailingError(buf, 1); - }, - .result => |f| f, - }; - this.redirection_fd = redirfd; - setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, .{ .fd = redirfd }); - }, - } - } else if (this.node.redirect.duplicate_out) { - if (this.node.redirect.stdout) { - spawn_args.stdio[stderr_no] = .{ .dup2 = .{ .out = .stderr, .to = .stdout } }; - } + } else { + const jsval = this.base.interpreter.jsobjs[val.idx]; + global.throw( + "Unknown JS value used in shell: {}", + .{jsval.fmtString(global)}, + ); + return; + } + }, + .atom => { + if (this.redirection_file.items.len == 0) { + this.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{spawn_args.argv.items[0] orelse ""}); + return; + } + const path = this.redirection_file.items[0..this.redirection_file.items.len -| 1 :0]; + log("EXPANDED REDIRECT: {s}\n", .{this.redirection_file.items[0..]}); + const perm = 0o666; + const flags = this.node.redirect.toFlags(); + const redirfd = switch (ShellSyscall.openat(this.base.shell.cwd_fd, path, flags, perm)) { + .err => |e| { + return this.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + }, + .result => |f| f, + }; + this.redirection_fd = CowFd.init(redirfd); + setStdioFromRedirect(&spawn_args.stdio, this.node.redirect, .{ .fd = redirfd }); + }, + } + } else if (this.node.redirect.duplicate_out) { + if (this.node.redirect.stdout) { + spawn_args.stdio[stderr_no] = .{ .dup2 = .{ .out = .stderr, .to = .stdout } }; + } - if (this.node.redirect.stderr) { - spawn_args.stdio[stdout_no] = .{ .dup2 = .{ .out = .stdout, .to = .stderr } }; - } + if (this.node.redirect.stderr) { + spawn_args.stdio[stdout_no] = .{ .dup2 = .{ .out = .stdout, .to = .stderr } }; } + } - const buffered_closed = BufferedIoClosed.fromStdio(&spawn_args.stdio); - log("cmd ({x}) set buffered closed => {any}", .{ @intFromPtr(this), buffered_closed }); + const buffered_closed = BufferedIoClosed.fromStdio(&spawn_args.stdio); + log("cmd ({x}) set buffered closed => {any}", .{ @intFromPtr(this), buffered_closed }); - this.exec = .{ .subproc = .{ - .child = undefined, - .buffered_closed = buffered_closed, - } }; - const subproc = switch (Subprocess.spawnAsync(this.base.interpreter.global, spawn_args, &this.exec.subproc.child)) { - .result => this.exec.subproc.child, - .err => |e| { - global_handle.get().actuallyThrow(e); - return; - }, - }; - subproc.ref(); - this.spawn_arena_freed = true; - arena.deinit(); + this.exec = .{ .subproc = .{ + .child = undefined, + .buffered_closed = buffered_closed, + } }; + const subproc = switch (Subprocess.spawnAsync(this.base.eventLoop(), spawn_args, &this.exec.subproc.child)) { + .result => this.exec.subproc.child, + .err => |*e| { + this.base.throw(e); + return; + }, + }; + subproc.ref(); + this.spawn_arena_freed = true; + arena.deinit(); - // if (this.cmd.stdout == .pipe and this.cmd.stdout.pipe == .buffer) { - // this.cmd.?.stdout.pipe.buffer.watch(); - // } + // if (this.cmd.stdout == .pipe and this.cmd.stdout.pipe == .buffer) { + // this.cmd.?.stdout.pipe.buffer.watch(); + // } + } + + fn setStdioFromRedirect(stdio: *[3]shell.subproc.Stdio, flags: ast.Cmd.RedirectFlags, val: shell.subproc.Stdio) void { + if (flags.stdin) { + stdio.*[stdin_no] = val; } - fn setStdioFromRedirect(stdio: *[3]shell.subproc.Stdio, flags: ast.Cmd.RedirectFlags, val: shell.subproc.Stdio) void { - if (flags.stdin) { - stdio.*[stdin_no] = val; + if (flags.duplicate_out) { + stdio.*[stdout_no] = val; + stdio.*[stderr_no] = val; + } else { + if (flags.stdout) { + stdio.*[stdout_no] = val; } - if (flags.duplicate_out) { - stdio.*[stdout_no] = val; + if (flags.stderr) { stdio.*[stderr_no] = val; - } else { - if (flags.stdout) { - stdio.*[stdout_no] = val; - } - - if (flags.stderr) { - stdio.*[stderr_no] = val; - } } } + } - /// Returns null if stdout is buffered - pub fn stdoutSlice(this: *Cmd) ?[]const u8 { - switch (this.exec) { - .none => return null, - .subproc => { - if (this.exec.subproc.buffered_closed.stdout != null and this.exec.subproc.buffered_closed.stdout.?.state == .closed) { - return this.exec.subproc.buffered_closed.stdout.?.state.closed.slice(); - } - return null; - }, - .bltn => { - switch (this.exec.bltn.stdout) { - .buf => return this.exec.bltn.stdout.buf.items[0..], - .arraybuf => return this.exec.bltn.stdout.arraybuf.buf.slice(), - .blob => return this.exec.bltn.stdout.blob.sharedView(), - else => return null, - } - }, - } + /// Returns null if stdout is buffered + pub fn stdoutSlice(this: *Cmd) ?[]const u8 { + switch (this.exec) { + .none => return null, + .subproc => { + if (this.exec.subproc.buffered_closed.stdout != null and this.exec.subproc.buffered_closed.stdout.?.state == .closed) { + return this.exec.subproc.buffered_closed.stdout.?.state.closed.slice(); + } + return null; + }, + .bltn => { + switch (this.exec.bltn.stdout) { + .buf => return this.exec.bltn.stdout.buf.items[0..], + .arraybuf => return this.exec.bltn.stdout.arraybuf.buf.slice(), + .blob => return this.exec.bltn.stdout.blob.sharedView(), + else => return null, + } + }, } + } - pub fn hasFinished(this: *Cmd) bool { - if (this.exit_code == null) return false; - if (this.exec != .none) { - if (this.exec == .subproc) return this.exec.subproc.buffered_closed.allClosed(); - return this.exec.bltn.ioAllClosed(); + pub fn hasFinished(this: *Cmd) bool { + log("Cmd(0x{x}) exit_code={any}", .{ @intFromPtr(this), this.exit_code }); + if (this.exit_code == null) return false; + if (this.exec != .none) { + if (this.exec == .subproc) { + return this.exec.subproc.buffered_closed.allClosed(); } - return true; + // return this.exec.bltn.ioAllClosed(); + return false; } + return true; + } - /// Called by Subprocess - pub fn onExit(this: *Cmd, exit_code: ExitCode) void { - log("cmd exit code={d} ({x})", .{ exit_code, @intFromPtr(this) }); - this.exit_code = exit_code; + /// Called by Subprocess + pub fn onExit(this: *Cmd, exit_code: ExitCode) void { + this.exit_code = exit_code; - const has_finished = this.hasFinished(); - if (has_finished) { - this.state = .done; - this.next(); - return; - // this.parent.childDone(this, exit_code); - } - // } else { - // this.cmd.?.stdout.pipe.buffer.readAll(); - // } + const has_finished = this.hasFinished(); + log("cmd exit code={d} has_finished={any} ({x})", .{ exit_code, has_finished, @intFromPtr(this) }); + if (has_finished) { + this.state = .done; + this.next(); + return; + // this.parent.childDone(this, exit_code); } + // } else { + // this.cmd.?.stdout.pipe.buffer.readAll(); + // } + } - // TODO check that this also makes sure that the poll ref is killed because if it isn't then this Cmd pointer will be stale and so when the event for pid exit happens it will cause crash - pub fn deinit(this: *Cmd) void { - log("cmd deinit {x}", .{@intFromPtr(this)}); - // this.base.shell.cmd_local_env.clearRetainingCapacity(); - if (this.redirection_fd != bun.invalid_fd) { - _ = Syscall.close(this.redirection_fd); - this.redirection_fd = bun.invalid_fd; - } - // if (this.exit_code != null) { - // if (this.cmd) |cmd| { - // _ = cmd.tryKill(9); - // cmd.unref(true); - // cmd.deinit(); - // } - // } + // TODO check that this also makes sure that the poll ref is killed because if it isn't then this Cmd pointer will be stale and so when the event for pid exit happens it will cause crash + pub fn deinit(this: *Cmd) void { + log("cmd deinit {x}", .{@intFromPtr(this)}); + // this.base.shell.cmd_local_env.clearRetainingCapacity(); + if (this.redirection_fd) |redirfd| { + this.redirection_fd = null; + redirfd.deref(); + } + // if (this.exit_code != null) { + // if (this.cmd) |cmd| { + // _ = cmd.tryKill(9); + // cmd.unref(true); + // cmd.deinit(); + // } + // } - // if (this.cmd) |cmd| { - // if (cmd.hasExited()) { - // cmd.unref(true); - // // cmd.deinit(); - // } else { - // _ = cmd.tryKill(9); - // cmd.unref(true); - // cmd.deinit(); - // } - // this.cmd = null; - // } + // if (this.cmd) |cmd| { + // if (cmd.hasExited()) { + // cmd.unref(true); + // // cmd.deinit(); + // } else { + // _ = cmd.tryKill(9); + // cmd.unref(true); + // cmd.deinit(); + // } + // this.cmd = null; + // } - log("WTF: {s}", .{@tagName(this.exec)}); - if (this.exec != .none) { - if (this.exec == .subproc) { - var cmd = this.exec.subproc.child; - if (cmd.hasExited()) { - cmd.unref(true); - // cmd.deinit(); - } else { - _ = cmd.tryKill(9); - cmd.unref(true); - cmd.deinit(); - } - this.exec.subproc.buffered_closed.deinit(GlobalHandle.init(this.base.interpreter.global).allocator()); + log("WTF: {s}", .{@tagName(this.exec)}); + if (this.exec != .none) { + if (this.exec == .subproc) { + var cmd = this.exec.subproc.child; + if (cmd.hasExited()) { + cmd.unref(true); + // cmd.deinit(); } else { - this.exec.bltn.deinit(); + _ = cmd.tryKill(9); + cmd.unref(true); + cmd.deinit(); } - this.exec = .none; - } - if (!this.spawn_arena_freed) { - log("Spawn arena free", .{}); - this.spawn_arena.deinit(); + this.exec.subproc.buffered_closed.deinit(this.base.eventLoop().allocator()); + } else { + this.exec.bltn.deinit(); } - this.freed = true; - this.base.interpreter.allocator.destroy(this); + this.exec = .none; } - pub fn bufferedInputClose(this: *Cmd) void { - this.exec.subproc.buffered_closed.close(this, .stdin); + if (!this.spawn_arena_freed) { + log("Spawn arena free", .{}); + this.spawn_arena.deinit(); } + this.freed = true; + this.io.deref(); + this.base.interpreter.allocator.destroy(this); + } - pub fn bufferedOutputClose(this: *Cmd, kind: Subprocess.OutKind) void { - switch (kind) { - .stdout => this.bufferedOutputCloseStdout(), - .stderr => this.bufferedOutputCloseStderr(), - } - if (this.hasFinished()) { - this.parent.childDone(this, this.exit_code orelse 0); - } - } + pub fn bufferedInputClose(this: *Cmd) void { + this.exec.subproc.buffered_closed.close(this, .stdin); + } - pub fn bufferedOutputCloseStdout(this: *Cmd) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.exec == .subproc); - } - log("cmd ({x}) close buffered stdout", .{@intFromPtr(this)}); - if (this.io.stdout == .std and this.io.stdout.std.captured != null and !this.node.redirect.redirectsElsewhere(.stdout)) { - var buf = this.io.stdout.std.captured.?; - buf.append(bun.default_allocator, this.exec.subproc.child.stdout.pipe.buffer.internal_buffer.slice()) catch bun.outOfMemory(); - } - this.exec.subproc.buffered_closed.close(this, .{ .stdout = &this.exec.subproc.child.stdout }); - this.exec.subproc.child.closeIO(.stdout); + pub fn bufferedOutputClose(this: *Cmd, kind: Subprocess.OutKind) void { + switch (kind) { + .stdout => this.bufferedOutputCloseStdout(), + .stderr => this.bufferedOutputCloseStderr(), } - - pub fn bufferedOutputCloseStderr(this: *Cmd) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.exec == .subproc); - } - log("cmd ({x}) close buffered stderr", .{@intFromPtr(this)}); - if (this.io.stderr == .std and this.io.stderr.std.captured != null and !this.node.redirect.redirectsElsewhere(.stderr)) { - var buf = this.io.stderr.std.captured.?; - buf.append(bun.default_allocator, this.exec.subproc.child.stderr.pipe.buffer.internal_buffer.slice()) catch bun.outOfMemory(); - } - this.exec.subproc.buffered_closed.close(this, .{ .stderr = &this.exec.subproc.child.stderr }); - this.exec.subproc.child.closeIO(.stderr); + if (this.hasFinished()) { + this.parent.childDone(this, this.exit_code orelse 0); } - }; - - pub const Builtin = struct { - kind: Kind, - stdin: BuiltinIO, - stdout: BuiltinIO, - stderr: BuiltinIO, - exit_code: ?ExitCode = null, - - export_env: *EnvMap, - cmd_local_env: *EnvMap, - - arena: *bun.ArenaAllocator, - /// The following are allocated with the above arena - args: *const std.ArrayList(?[*:0]const u8), - args_slice: ?[]const [:0]const u8 = null, - cwd: bun.FileDescriptor, + } - impl: union(Kind) { - @"export": Export, - cd: Cd, - echo: Echo, - pwd: Pwd, - which: Which, - rm: Rm, - mv: Mv, - ls: Ls, - }, + pub fn bufferedOutputCloseStdout(this: *Cmd) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.exec == .subproc); + } + log("cmd ({x}) close buffered stdout", .{@intFromPtr(this)}); + if (this.io.stdout == .fd and this.io.stdout.fd.captured != null and !this.node.redirect.redirectsElsewhere(.stdout)) { + var buf = this.io.stdout.fd.captured.?; + const the_slice = this.exec.subproc.child.stdout.pipe.slice(); + buf.append(bun.default_allocator, the_slice) catch bun.outOfMemory(); + } + this.exec.subproc.buffered_closed.close(this, .{ .stdout = &this.exec.subproc.child.stdout }); + this.exec.subproc.child.closeIO(.stdout); + } - const Result = @import("../result.zig").Result; + pub fn bufferedOutputCloseStderr(this: *Cmd) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.exec == .subproc); + } + log("cmd ({x}) close buffered stderr", .{@intFromPtr(this)}); + if (this.io.stderr == .fd and this.io.stderr.fd.captured != null and !this.node.redirect.redirectsElsewhere(.stderr)) { + var buf = this.io.stderr.fd.captured.?; + buf.append(bun.default_allocator, this.exec.subproc.child.stderr.pipe.slice()) catch bun.outOfMemory(); + } + this.exec.subproc.buffered_closed.close(this, .{ .stderr = &this.exec.subproc.child.stderr }); + this.exec.subproc.child.closeIO(.stderr); + } + }; - pub const Kind = enum { - @"export", - cd, - echo, - pwd, - which, - rm, - mv, - ls, + pub const Builtin = struct { + kind: Kind, + stdin: BuiltinIO.Input, + stdout: BuiltinIO.Output, + stderr: BuiltinIO.Output, + exit_code: ?ExitCode = null, + + export_env: *EnvMap, + cmd_local_env: *EnvMap, + + arena: *bun.ArenaAllocator, + /// The following are allocated with the above arena + args: *const std.ArrayList(?[*:0]const u8), + args_slice: ?[]const [:0]const u8 = null, + cwd: bun.FileDescriptor, + + impl: union(Kind) { + cat: Cat, + touch: Touch, + mkdir: Mkdir, + @"export": Export, + cd: Cd, + echo: Echo, + pwd: Pwd, + which: Which, + rm: Rm, + mv: Mv, + ls: Ls, + }, + + const Result = @import("../result.zig").Result; + + pub const Kind = enum { + cat, + touch, + mkdir, + @"export", + cd, + echo, + pwd, + which, + rm, + mv, + ls, + + pub fn parentType(this: Kind) type { + _ = this; + } - pub fn parentType(this: Kind) type { - _ = this; - } + pub fn usageString(this: Kind) []const u8 { + return switch (this) { + .cat => "usage: cat [-belnstuv] [file ...]\n", + .touch => "usage: touch [-A [-][[hh]mm]SS] [-achm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]]\n [-d YYYY-MM-DDThh:mm:SS[.frac][tz]] file ...\n", + .mkdir => "usage: mkdir [-pv] [-m mode] directory_name ...\n", + .@"export" => "", + .cd => "", + .echo => "", + .pwd => "", + .which => "", + .rm => "usage: rm [-f | -i] [-dIPRrvWx] file ...\n unlink [--] file\n", + .mv => "usage: mv [-f | -i | -n] [-hv] source target\n mv [-f | -i | -n] [-v] source ... directory\n", + .ls => "usage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n", + }; + } - pub fn usageString(this: Kind) []const u8 { - return switch (this) { - .@"export" => "", - .cd => "", - .echo => "", - .pwd => "", - .which => "", - .rm => "usage: rm [-f | -i] [-dIPRrvWx] file ...\n unlink [--] file\n", - .mv => "usage: mv [-f | -i | -n] [-hv] source target\n mv [-f | -i | -n] [-v] source ... directory\n", - .ls => "usage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n", - }; - } + pub fn asString(this: Kind) []const u8 { + return switch (this) { + .cat => "cat", + .touch => "touch", + .mkdir => "mkdir", + .@"export" => "export", + .cd => "cd", + .echo => "echo", + .pwd => "pwd", + .which => "which", + .rm => "rm", + .mv => "mv", + .ls => "ls", + }; + } - pub fn asString(this: Kind) []const u8 { - return switch (this) { - .@"export" => "export", - .cd => "cd", - .echo => "echo", - .pwd => "pwd", - .which => "which", - .rm => "rm", - .mv => "mv", - .ls => "ls", - }; + pub fn fromStr(str: []const u8) ?Builtin.Kind { + if (!bun.Environment.isWindows) { + if (bun.strings.eqlComptime(str, "cat")) { + log("Cat builtin disabled on posix for now", .{}); + return null; + } } - - pub fn fromStr(str: []const u8) ?Builtin.Kind { - const tyinfo = @typeInfo(Builtin.Kind); - inline for (tyinfo.Enum.fields) |field| { - if (bun.strings.eqlComptime(str, field.name)) { - return comptime std.meta.stringToEnum(Builtin.Kind, field.name).?; - } + @setEvalBranchQuota(5000); + const tyinfo = @typeInfo(Builtin.Kind); + inline for (tyinfo.Enum.fields) |field| { + if (bun.strings.eqlComptime(str, field.name)) { + return comptime std.meta.stringToEnum(Builtin.Kind, field.name).?; } - return null; } - }; + return null; + } + }; + pub const BuiltinIO = struct { /// in the case of array buffer we simply need to write to the pointer /// in the case of blob, we write to the file descriptor - pub const BuiltinIO = union(enum) { - fd: bun.FileDescriptor, + pub const Output = union(enum) { + fd: struct { writer: *IOWriter, captured: ?*bun.ByteList = null }, + /// array list not owned by this type buf: std.ArrayList(u8), - captured: struct { - out_kind: enum { stdout, stderr }, - bytelist: *bun.ByteList, - }, arraybuf: ArrayBuf, - blob: *bun.JSC.WebCore.Blob, + blob: *Blob, ignore, - const ArrayBuf = struct { - buf: JSC.ArrayBuffer.Strong, - i: u32 = 0, - }; - - pub fn asFd(this: *BuiltinIO) ?bun.FileDescriptor { - return switch (this.*) { - .fd => this.fd, - .captured => if (this.captured.out_kind == .stdout) bun.STDOUT_FD else bun.STDERR_FD, - else => null, - }; - } - - pub fn expectFd(this: *BuiltinIO) bun.FileDescriptor { - return switch (this.*) { - .fd => this.fd, - .captured => if (this.captured.out_kind == .stdout) bun.STDOUT_FD else bun.STDERR_FD, - else => @panic("No fd"), - }; - } + const FdOutput = struct { + writer: *IOWriter, + captured: ?*bun.ByteList = null, - pub fn isClosed(this: *BuiltinIO) bool { - switch (this.*) { - .fd => { - return this.fd != bun.invalid_fd; - }, - .buf => { - return true; - // try this.buf.deinit(allocator); - }, - else => return true, - } - } + // pub fn + }; - pub fn deinit(this: *BuiltinIO) void { + pub fn ref(this: *Output) *Output { switch (this.*) { - .buf => { - this.buf.deinit(); - }, .fd => { - if (this.fd != bun.invalid_fd and this.fd != bun.STDIN_FD) { - _ = Syscall.close(this.fd); - this.fd = bun.invalid_fd; - } - }, - .blob => |blob| { - blob.deinit(); + this.fd.writer.ref(); }, + .blob => this.blob.ref(), else => {}, } + return this; } - pub fn close(this: *BuiltinIO) void { + pub fn deref(this: *Output) void { switch (this.*) { .fd => { - if (this.fd != bun.invalid_fd) { - closefd(this.fd); - this.fd = bun.invalid_fd; - } + this.fd.writer.deref(); }, - .buf => {}, + .blob => this.blob.deref(), else => {}, } } - pub fn needsIO(this: *BuiltinIO) bool { + pub fn needsIO(this: *Output) bool { return switch (this.*) { - .fd, .captured => true, + .fd => true, else => false, }; } - }; - pub fn argsSlice(this: *Builtin) []const [*:0]const u8 { - const args_raw = this.args.items[1..]; - const args_len = std.mem.indexOfScalar(?[*:0]const u8, args_raw, null) orelse @panic("bad"); - if (args_len == 0) - return &[_][*:0]const u8{}; - - const args_ptr = args_raw.ptr; - return @as([*][*:0]const u8, @ptrCast(args_ptr))[0..args_len]; - } - - pub inline fn callImpl(this: *Builtin, comptime Ret: type, comptime field: []const u8, args_: anytype) Ret { - return switch (this.kind) { - .@"export" => this.callImplWithType(Export, Ret, "export", field, args_), - .echo => this.callImplWithType(Echo, Ret, "echo", field, args_), - .cd => this.callImplWithType(Cd, Ret, "cd", field, args_), - .which => this.callImplWithType(Which, Ret, "which", field, args_), - .rm => this.callImplWithType(Rm, Ret, "rm", field, args_), - .pwd => this.callImplWithType(Pwd, Ret, "pwd", field, args_), - .mv => this.callImplWithType(Mv, Ret, "mv", field, args_), - .ls => this.callImplWithType(Ls, Ret, "ls", field, args_), - }; - } + pub fn enqueueFmtBltn( + this: *@This(), + ptr: anytype, + comptime kind: ?Interpreter.Builtin.Kind, + comptime fmt_: []const u8, + args: anytype, + ) void { + if (bun.Environment.allow_assert) std.debug.assert(this.* == .fd); + this.fd.writer.enqueueFmtBltn(ptr, this.fd.captured, kind, fmt_, args); + } - fn callImplWithType(this: *Builtin, comptime Impl: type, comptime Ret: type, comptime union_field: []const u8, comptime field: []const u8, args_: anytype) Ret { - const self = &@field(this.impl, union_field); - const args = brk: { - var args: std.meta.ArgsTuple(@TypeOf(@field(Impl, field))) = undefined; - args[0] = self; + pub fn enqueue(this: *@This(), ptr: anytype, buf: []const u8) void { + if (bun.Environment.allow_assert) std.debug.assert(this.* == .fd); + this.fd.writer.enqueue(ptr, this.fd.captured, buf); + } + }; - var i: usize = 1; - inline for (args_) |a| { - args[i] = a; - i += 1; - } + pub const Input = union(enum) { + fd: *IOReader, + /// array list not ownedby this type + buf: std.ArrayList(u8), + arraybuf: ArrayBuf, + blob: *Blob, + ignore, - break :brk args; - }; - return @call(.auto, @field(Impl, field), args); - } + pub fn ref(this: *Input) *Input { + switch (this.*) { + .fd => { + this.fd.ref(); + }, + .blob => this.blob.ref(), + else => {}, + } + return this; + } + + pub fn deref(this: *Input) void { + switch (this.*) { + .fd => { + this.fd.deref(); + }, + .blob => this.blob.deref(), + else => {}, + } + } + + pub fn needsIO(this: *Input) bool { + return switch (this.*) { + .fd => true, + else => false, + }; + } + }; + + const ArrayBuf = struct { + buf: JSC.ArrayBuffer.Strong, + i: u32 = 0, + }; + + const Blob = struct { + ref_count: usize = 1, + blob: bun.JSC.WebCore.Blob, + pub usingnamespace bun.NewRefCounted(Blob, Blob.deinit); + + pub fn deinit(this: *Blob) void { + this.blob.deinit(); + bun.destroy(this); + } + }; + }; + + pub fn argsSlice(this: *Builtin) []const [*:0]const u8 { + const args_raw = this.args.items[1..]; + const args_len = std.mem.indexOfScalar(?[*:0]const u8, args_raw, null) orelse @panic("bad"); + if (args_len == 0) + return &[_][*:0]const u8{}; + + const args_ptr = args_raw.ptr; + return @as([*][*:0]const u8, @ptrCast(args_ptr))[0..args_len]; + } + + pub inline fn callImpl(this: *Builtin, comptime Ret: type, comptime field: []const u8, args_: anytype) Ret { + return switch (this.kind) { + .cat => this.callImplWithType(Cat, Ret, "cat", field, args_), + .touch => this.callImplWithType(Touch, Ret, "touch", field, args_), + .mkdir => this.callImplWithType(Mkdir, Ret, "mkdir", field, args_), + .@"export" => this.callImplWithType(Export, Ret, "export", field, args_), + .echo => this.callImplWithType(Echo, Ret, "echo", field, args_), + .cd => this.callImplWithType(Cd, Ret, "cd", field, args_), + .which => this.callImplWithType(Which, Ret, "which", field, args_), + .rm => this.callImplWithType(Rm, Ret, "rm", field, args_), + .pwd => this.callImplWithType(Pwd, Ret, "pwd", field, args_), + .mv => this.callImplWithType(Mv, Ret, "mv", field, args_), + .ls => this.callImplWithType(Ls, Ret, "ls", field, args_), + }; + } + + fn callImplWithType(this: *Builtin, comptime Impl: type, comptime Ret: type, comptime union_field: []const u8, comptime field: []const u8, args_: anytype) Ret { + const self = &@field(this.impl, union_field); + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(@field(Impl, field))) = undefined; + args[0] = self; + + var i: usize = 1; + inline for (args_) |a| { + args[i] = a; + i += 1; + } + + break :brk args; + }; + return @call(.auto, @field(Impl, field), args); + } + + pub inline fn allocator(this: *Builtin) Allocator { + return this.parentCmd().base.interpreter.allocator; + } + + pub fn init( + cmd: *Cmd, + interpreter: *ThisInterpreter, + kind: Kind, + arena: *bun.ArenaAllocator, + node: *const ast.Cmd, + args: *const std.ArrayList(?[*:0]const u8), + export_env: *EnvMap, + cmd_local_env: *EnvMap, + cwd: bun.FileDescriptor, + io: *IO, + comptime in_cmd_subst: bool, + ) CoroutineResult { + const stdin: BuiltinIO.Input = switch (io.stdin) { + .fd => |fd| .{ .fd = fd.refSelf() }, + .ignore => .ignore, + }; + const stdout: BuiltinIO.Output = switch (io.stdout) { + .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } }, + .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) }, + .ignore => .ignore, + // .std => if (io.stdout.std.captured) |bytelist| .{ .captured = .{ .out_kind = .stdout, .bytelist = bytelist } } else .{ .fd = bun.STDOUT_FD }, + // .fd => |fd| .{ .fd = fd }, + // .pipe => .{ .buf = std.ArrayList(u8).init(interpreter.allocator) }, + // .ignore => .ignore, + }; + const stderr: BuiltinIO.Output = switch (io.stderr) { + .fd => |val| .{ .fd = .{ .writer = val.writer.refSelf(), .captured = val.captured } }, + .pipe => .{ .buf = std.ArrayList(u8).init(bun.default_allocator) }, + .ignore => .ignore, + }; + + cmd.exec = .{ + .bltn = Builtin{ + .kind = kind, + .stdin = stdin, + .stdout = stdout, + .stderr = stderr, + .exit_code = null, + .arena = arena, + .args = args, + .export_env = export_env, + .cmd_local_env = cmd_local_env, + .cwd = cwd, + .impl = undefined, + }, + }; - pub inline fn allocator(this: *Builtin) Allocator { - return this.parentCmd().base.interpreter.allocator; + switch (kind) { + .cat => { + cmd.exec.bltn.impl = .{ + .cat = Cat{ .bltn = &cmd.exec.bltn }, + }; + }, + .touch => { + cmd.exec.bltn.impl = .{ + .touch = Touch{ .bltn = &cmd.exec.bltn }, + }; + }, + .mkdir => { + cmd.exec.bltn.impl = .{ + .mkdir = Mkdir{ .bltn = &cmd.exec.bltn }, + }; + }, + .@"export" => { + cmd.exec.bltn.impl = .{ + .@"export" = Export{ .bltn = &cmd.exec.bltn }, + }; + }, + .rm => { + cmd.exec.bltn.impl = .{ + .rm = Rm{ + .bltn = &cmd.exec.bltn, + .opts = .{}, + }, + }; + }, + .echo => { + cmd.exec.bltn.impl = .{ + .echo = Echo{ + .bltn = &cmd.exec.bltn, + .output = std.ArrayList(u8).init(arena.allocator()), + }, + }; + }, + .cd => { + cmd.exec.bltn.impl = .{ + .cd = Cd{ + .bltn = &cmd.exec.bltn, + }, + }; + }, + .which => { + cmd.exec.bltn.impl = .{ + .which = Which{ + .bltn = &cmd.exec.bltn, + }, + }; + }, + .pwd => { + cmd.exec.bltn.impl = .{ + .pwd = Pwd{ .bltn = &cmd.exec.bltn }, + }; + }, + .mv => { + cmd.exec.bltn.impl = .{ + .mv = Mv{ .bltn = &cmd.exec.bltn }, + }; + }, + .ls => { + cmd.exec.bltn.impl = .{ + .ls = Ls{ + .bltn = &cmd.exec.bltn, + }, + }; + }, } - pub fn init( - cmd: *Cmd, - interpreter: *ThisInterpreter, - kind: Kind, - arena: *bun.ArenaAllocator, - node: *const ast.Cmd, - args: *const std.ArrayList(?[*:0]const u8), - export_env: *EnvMap, - cmd_local_env: *EnvMap, - cwd: bun.FileDescriptor, - io_: *IO, - comptime in_cmd_subst: bool, - ) CoroutineResult { - const io = io_.*; - - const stdin: Builtin.BuiltinIO = switch (io.stdin) { - .std => .{ .fd = bun.STDIN_FD }, - .fd => |fd| .{ .fd = fd }, - .pipe => .{ .buf = std.ArrayList(u8).init(interpreter.allocator) }, - .ignore => .ignore, - }; - const stdout: Builtin.BuiltinIO = switch (io.stdout) { - .std => if (io.stdout.std.captured) |bytelist| .{ .captured = .{ .out_kind = .stdout, .bytelist = bytelist } } else .{ .fd = bun.STDOUT_FD }, - .fd => |fd| .{ .fd = fd }, - .pipe => .{ .buf = std.ArrayList(u8).init(interpreter.allocator) }, - .ignore => .ignore, - }; - const stderr: Builtin.BuiltinIO = switch (io.stderr) { - .std => if (io.stderr.std.captured) |bytelist| .{ .captured = .{ .out_kind = .stderr, .bytelist = bytelist } } else .{ .fd = bun.STDERR_FD }, - .fd => |fd| .{ .fd = fd }, - .pipe => .{ .buf = std.ArrayList(u8).init(interpreter.allocator) }, - .ignore => .ignore, - }; + if (node.redirect_file) |file| brk: { + if (comptime in_cmd_subst) { + if (node.redirect.stdin) { + stdin = .ignore; + } - cmd.exec = .{ - .bltn = Builtin{ - .kind = kind, - .stdin = stdin, - .stdout = stdout, - .stderr = stderr, - .exit_code = null, - .arena = arena, - .args = args, - .export_env = export_env, - .cmd_local_env = cmd_local_env, - .cwd = cwd, - .impl = undefined, - }, - }; + if (node.redirect.stdout) { + stdout = .ignore; + } - switch (kind) { - .@"export" => { - cmd.exec.bltn.impl = .{ - .@"export" = Export{ .bltn = &cmd.exec.bltn }, - }; - }, - .rm => { - cmd.exec.bltn.impl = .{ - .rm = Rm{ - .bltn = &cmd.exec.bltn, - .opts = .{}, - }, - }; - }, - .echo => { - cmd.exec.bltn.impl = .{ - .echo = Echo{ - .bltn = &cmd.exec.bltn, - .output = std.ArrayList(u8).init(arena.allocator()), - }, - }; - }, - .cd => { - cmd.exec.bltn.impl = .{ - .cd = Cd{ - .bltn = &cmd.exec.bltn, - }, - }; - }, - .which => { - cmd.exec.bltn.impl = .{ - .which = Which{ - .bltn = &cmd.exec.bltn, - }, - }; - }, - .pwd => { - cmd.exec.bltn.impl = .{ - .pwd = Pwd{ .bltn = &cmd.exec.bltn }, - }; - }, - .mv => { - cmd.exec.bltn.impl = .{ - .mv = Mv{ .bltn = &cmd.exec.bltn }, - }; - }, - .ls => { - cmd.exec.bltn.impl = .{ - .ls = Ls{ - .bltn = &cmd.exec.bltn, - }, - }; - }, + if (node.redirect.stderr) { + stdout = .ignore; + } + + break :brk; } - if (node.redirect_file) |file| brk: { - if (comptime in_cmd_subst) { + switch (file) { + .atom => { + if (cmd.redirection_file.items.len == 0) { + cmd.writeFailingError("bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)}); + return .yield; + } + const path = cmd.redirection_file.items[0..cmd.redirection_file.items.len -| 1 :0]; + log("EXPANDED REDIRECT: {s}\n", .{cmd.redirection_file.items[0..]}); + const perm = 0o666; + const flags = node.redirect.toFlags(); + const redirfd = switch (ShellSyscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) { + .err => |e| { + cmd.writeFailingError("bun: {s}: {s}", .{ e.toSystemError().message, path }); + return .yield; + }, + .result => |f| f, + // cmd.redirection_fd = redirfd; + }; if (node.redirect.stdin) { - stdin = .ignore; + cmd.exec.bltn.stdin.deref(); + cmd.exec.bltn.stdin = .{ .fd = IOReader.init(redirfd, cmd.base.eventLoop()) }; } - if (node.redirect.stdout) { - stdout = .ignore; + cmd.exec.bltn.stdout.deref(); + cmd.exec.bltn.stdout = .{ .fd = .{ .writer = IOWriter.init(redirfd, cmd.base.eventLoop()) } }; } - if (node.redirect.stderr) { - stdout = .ignore; + cmd.exec.bltn.stderr.deref(); + cmd.exec.bltn.stderr = .{ .fd = .{ .writer = IOWriter.init(redirfd, cmd.base.eventLoop()) } }; } + }, + .jsbuf => |val| { + const globalObject = interpreter.event_loop.js.global; + if (interpreter.jsobjs[file.jsbuf.idx].asArrayBuffer(globalObject)) |buf| { + const arraybuf: BuiltinIO.ArrayBuf = .{ .buf = JSC.ArrayBuffer.Strong{ + .array_buffer = buf, + .held = JSC.Strong.create(buf.value, globalObject), + }, .i = 0 }; - break :brk; - } - - switch (file) { - .atom => { - if (cmd.redirection_file.items.len == 0) { - const buf = std.fmt.allocPrint(arena.allocator(), "bun: ambiguous redirect: at `{s}`\n", .{@tagName(kind)}) catch bun.outOfMemory(); - cmd.writeFailingError(buf, 1); - return .yield; - } - const path = cmd.redirection_file.items[0..cmd.redirection_file.items.len -| 1 :0]; - log("EXPANDED REDIRECT: {s}\n", .{cmd.redirection_file.items[0..]}); - const perm = 0o666; - const flags = node.redirect.toFlags(); - const redirfd = switch (Syscall.openat(cmd.base.shell.cwd_fd, path, flags, perm)) { - .err => |e| { - const buf = std.fmt.allocPrint(arena.allocator(), "bun: {s}: {s}", .{ e.toSystemError().message, path }) catch bun.outOfMemory(); - cmd.writeFailingError(buf, 1); - return .yield; - }, - .result => |f| f, - }; - // cmd.redirection_fd = redirfd; if (node.redirect.stdin) { - cmd.exec.bltn.stdin = .{ .fd = redirfd }; + cmd.exec.bltn.stdin.deref(); + cmd.exec.bltn.stdin = .{ .arraybuf = arraybuf }; } + if (node.redirect.stdout) { - cmd.exec.bltn.stdout = .{ .fd = redirfd }; + cmd.exec.bltn.stdout.deref(); + cmd.exec.bltn.stdout = .{ .arraybuf = arraybuf }; } + if (node.redirect.stderr) { - cmd.exec.bltn.stderr = .{ .fd = redirfd }; + cmd.exec.bltn.stderr.deref(); + cmd.exec.bltn.stderr = .{ .arraybuf = arraybuf }; + } + } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Body.Value)) |body| { + if ((node.redirect.stdout or node.redirect.stderr) and !(body.* == .Blob and !body.Blob.needsToReadFile())) { + // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary. + cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}); + return .yield; } - }, - .jsbuf => |val| { - if (comptime EventLoopKind == .mini) @panic("This should nevver happened"); - if (interpreter.jsobjs[file.jsbuf.idx].asArrayBuffer(interpreter.global)) |buf| { - const builtinio: Builtin.BuiltinIO = .{ .arraybuf = .{ .buf = JSC.ArrayBuffer.Strong{ - .array_buffer = buf, - .held = JSC.Strong.create(buf.value, interpreter.global), - }, .i = 0 } }; - - if (node.redirect.stdin) { - cmd.exec.bltn.stdin = builtinio; - } - if (node.redirect.stdout) { - cmd.exec.bltn.stdout = builtinio; - } + var original_blob = body.use(); + defer original_blob.deinit(); - if (node.redirect.stderr) { - cmd.exec.bltn.stderr = builtinio; - } - } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Blob)) |blob| { - const builtinio: Builtin.BuiltinIO = .{ .blob = bun.newWithAlloc(arena.allocator(), JSC.WebCore.Blob, blob.dupe()) }; + const blob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{ + .blob = original_blob.dupe(), + }); - if (node.redirect.stdin) { - cmd.exec.bltn.stdin = builtinio; - } + if (node.redirect.stdin) { + cmd.exec.bltn.stdin.deref(); + cmd.exec.bltn.stdin = .{ .blob = blob }; + } - if (node.redirect.stdout) { - cmd.exec.bltn.stdout = builtinio; - } + if (node.redirect.stdout) { + cmd.exec.bltn.stdout.deref(); + cmd.exec.bltn.stdout = .{ .blob = blob }; + } - if (node.redirect.stderr) { - cmd.exec.bltn.stderr = builtinio; - } - } else { - const jsval = cmd.base.interpreter.jsobjs[val.idx]; - global_handle.get().globalThis.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(global_handle.get().globalThis)}); + if (node.redirect.stderr) { + cmd.exec.bltn.stderr.deref(); + cmd.exec.bltn.stderr = .{ .blob = blob }; + } + } else if (interpreter.jsobjs[file.jsbuf.idx].as(JSC.WebCore.Blob)) |blob| { + if ((node.redirect.stdout or node.redirect.stderr) and !blob.needsToReadFile()) { + // TODO: Locked->stream -> file -> blob conversion via .toBlobIfPossible() except we want to avoid modifying the Response/Request if unnecessary. + cmd.base.interpreter.event_loop.js.global.throw("Cannot redirect stdout/stderr to an immutable blob. Expected a file", .{}); return .yield; } - }, - } - } else if (node.redirect.duplicate_out) { - if (node.redirect.stdout) { - cmd.exec.bltn.stderr = cmd.exec.bltn.stdout; - } - if (node.redirect.stderr) { - cmd.exec.bltn.stdout = cmd.exec.bltn.stderr; - } + const theblob: *BuiltinIO.Blob = bun.new(BuiltinIO.Blob, .{ .blob = blob.dupe() }); + + if (node.redirect.stdin) { + cmd.exec.bltn.stdin.deref(); + cmd.exec.bltn.stdin = .{ .blob = theblob }; + } else if (node.redirect.stdout) { + cmd.exec.bltn.stdout.deref(); + cmd.exec.bltn.stdout = .{ .blob = theblob }; + } else if (node.redirect.stderr) { + cmd.exec.bltn.stderr.deref(); + cmd.exec.bltn.stderr = .{ .blob = theblob }; + } + } else { + const jsval = cmd.base.interpreter.jsobjs[val.idx]; + cmd.base.interpreter.event_loop.js.global.throw("Unknown JS value used in shell: {}", .{jsval.fmtString(globalObject)}); + return .yield; + } + }, + } + } else if (node.redirect.duplicate_out) { + if (node.redirect.stdout) { + cmd.exec.bltn.stderr.deref(); + cmd.exec.bltn.stderr = cmd.exec.bltn.stdout.ref().*; } - return .cont; + if (node.redirect.stderr) { + cmd.exec.bltn.stdout.deref(); + cmd.exec.bltn.stdout = cmd.exec.bltn.stderr.ref().*; + } } - pub inline fn parentCmd(this: *Builtin) *Cmd { - const union_ptr = @fieldParentPtr(Cmd.Exec, "bltn", this); - return @fieldParentPtr(Cmd, "exec", union_ptr); - } + return .cont; + } - pub fn done(this: *Builtin, exit_code: ExitCode) void { - // if (comptime bun.Environment.allow_assert) { - // std.debug.assert(this.exit_code != null); - // } - this.exit_code = exit_code; + pub inline fn eventLoop(this: *const Builtin) JSC.EventLoopHandle { + return this.parentCmd().base.eventLoop(); + } - var cmd = this.parentCmd(); - log("builtin done ({s}: exit={d}) cmd to free: ({x})", .{ @tagName(this.kind), exit_code, @intFromPtr(cmd) }); - cmd.exit_code = this.exit_code.?; + pub inline fn throw(this: *const Builtin, err: *const bun.shell.ShellErr) void { + this.parentCmd().base.throw(err); + } - // Aggregate output data if shell state is piped and this cmd is piped - if (cmd.io.stdout == .pipe and cmd.base.shell.io.stdout == .pipe and this.stdout == .buf) { - cmd.base.shell.buffered_stdout().append(bun.default_allocator, this.stdout.buf.items[0..]) catch bun.outOfMemory(); - } - // Aggregate output data if shell state is piped and this cmd is piped - if (cmd.io.stderr == .pipe and cmd.base.shell.io.stderr == .pipe and this.stderr == .buf) { - cmd.base.shell.buffered_stderr().append(bun.default_allocator, this.stderr.buf.items[0..]) catch bun.outOfMemory(); - } + pub inline fn parentCmd(this: *const Builtin) *const Cmd { + const union_ptr = @fieldParentPtr(Cmd.Exec, "bltn", this); + return @fieldParentPtr(Cmd, "exec", union_ptr); + } - cmd.parent.childDone(cmd, this.exit_code.?); - } + pub inline fn parentCmdMut(this: *Builtin) *Cmd { + const union_ptr = @fieldParentPtr(Cmd.Exec, "bltn", this); + return @fieldParentPtr(Cmd, "exec", union_ptr); + } - pub fn start(this: *Builtin) Maybe(void) { - switch (this.callImpl(Maybe(void), "start", .{})) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } + pub fn done(this: *Builtin, exit_code: anytype) void { + // if (comptime bun.Environment.allow_assert) { + // std.debug.assert(this.exit_code != null); + // } + const code: ExitCode = switch (@TypeOf(exit_code)) { + bun.C.E => @intFromEnum(exit_code), + u1, u8, u16 => exit_code, + comptime_int => exit_code, + else => @compileError("Invalid type: " ++ @typeName(@TypeOf(exit_code))), + }; + this.exit_code = code; - return Maybe(void).success; + var cmd = this.parentCmdMut(); + log("builtin done ({s}: exit={d}) cmd to free: ({x})", .{ @tagName(this.kind), code, @intFromPtr(cmd) }); + cmd.exit_code = this.exit_code.?; + + // Aggregate output data if shell state is piped and this cmd is piped + if (cmd.io.stdout == .pipe and cmd.io.stdout == .pipe and this.stdout == .buf) { + cmd.base.shell.buffered_stdout().append(bun.default_allocator, this.stdout.buf.items[0..]) catch bun.outOfMemory(); + } + // Aggregate output data if shell state is piped and this cmd is piped + if (cmd.io.stderr == .pipe and cmd.io.stderr == .pipe and this.stderr == .buf) { + cmd.base.shell.buffered_stderr().append(bun.default_allocator, this.stderr.buf.items[0..]) catch bun.outOfMemory(); } - pub fn deinit(this: *Builtin) void { - this.callImpl(void, "deinit", .{}); + cmd.parent.childDone(cmd, this.exit_code.?); + } - // No need to free it because it belongs to the parent cmd - // _ = Syscall.close(this.cwd); + pub fn start(this: *Builtin) Maybe(void) { + switch (this.callImpl(Maybe(void), "start", .{})) { + .err => |e| return Maybe(void).initErr(e), + .result => {}, + } - this.stdout.deinit(); - this.stderr.deinit(); - this.stdin.deinit(); + return Maybe(void).success; + } - // this.arena.deinit(); - } + pub fn deinit(this: *Builtin) void { + this.callImpl(void, "deinit", .{}); - // pub fn writeNonBlocking(this: *Builtin, comptime io_kind: @Type(.EnumLiteral), buf: []u8) Maybe(usize) { - // if (comptime io_kind != .stdout and io_kind != .stderr) { - // @compileError("Bad IO" ++ @tagName(io_kind)); - // } + // No need to free it because it belongs to the parent cmd + // _ = Syscall.close(this.cwd); - // var io: *BuiltinIO = &@field(this, @tagName(io_kind)); - // switch (io.*) { - // .buf, .arraybuf => { - // return this.writeNoIO(io_kind, buf); - // }, - // .fd => { - // return Syscall.write(io.fd, buf); - // }, - // } - // } + this.stdout.deref(); + this.stderr.deref(); + this.stdin.deref(); - /// If the stdout/stderr is supposed to be captured then get the bytelist associated with that - pub fn stdBufferedBytelist(this: *Builtin, comptime io_kind: @Type(.EnumLiteral)) ?*bun.ByteList { - if (comptime io_kind != .stdout and io_kind != .stderr) { - @compileError("Bad IO" ++ @tagName(io_kind)); - } + // this.arena.deinit(); + } - const io: *BuiltinIO = &@field(this, @tagName(io_kind)); - return switch (io.*) { - .captured => if (comptime io_kind == .stdout) this.parentCmd().base.shell.buffered_stdout() else this.parentCmd().base.shell.buffered_stderr(), - else => null, - }; - } + // pub fn writeNonBlocking(this: *Builtin, comptime io_kind: @Type(.EnumLiteral), buf: []u8) Maybe(usize) { + // if (comptime io_kind != .stdout and io_kind != .stderr) { + // @compileError("Bad IO" ++ @tagName(io_kind)); + // } + + // var io: *BuiltinIO = &@field(this, @tagName(io_kind)); + // switch (io.*) { + // .buf, .arraybuf => { + // return this.writeNoIO(io_kind, buf); + // }, + // .fd => { + // return Syscall.write(io.fd, buf); + // }, + // } + // } + + /// If the stdout/stderr is supposed to be captured then get the bytelist associated with that + pub fn stdBufferedBytelist(this: *Builtin, comptime io_kind: @Type(.EnumLiteral)) ?*bun.ByteList { + if (comptime io_kind != .stdout and io_kind != .stderr) { + @compileError("Bad IO" ++ @tagName(io_kind)); + } + + const io: *BuiltinIO = &@field(this, @tagName(io_kind)); + return switch (io.*) { + .captured => if (comptime io_kind == .stdout) this.parentCmd().base.shell.buffered_stdout() else this.parentCmd().base.shell.buffered_stderr(), + else => null, + }; + } - pub fn writeNoIO(this: *Builtin, comptime io_kind: @Type(.EnumLiteral), buf: []const u8) Maybe(usize) { - if (comptime io_kind != .stdout and io_kind != .stderr) { - @compileError("Bad IO" ++ @tagName(io_kind)); - } + pub fn readStdinNoIO(this: *Builtin) []const u8 { + return switch (this.stdin) { + .arraybuf => |buf| buf.buf.slice(), + .buf => |buf| buf.items[0..], + .blob => |blob| blob.blob.sharedView(), + else => "", + }; + } - if (buf.len == 0) return .{ .result = 0 }; + pub fn writeNoIO(this: *Builtin, comptime io_kind: @Type(.EnumLiteral), buf: []const u8) usize { + if (comptime io_kind != .stdout and io_kind != .stderr) { + @compileError("Bad IO" ++ @tagName(io_kind)); + } - var io: *BuiltinIO = &@field(this, @tagName(io_kind)); + if (buf.len == 0) return 0; - switch (io.*) { - .captured, .fd => @panic("writeNoIO can't write to a file descriptor"), - .buf => { - log("{s} write to buf len={d} str={s}{s}\n", .{ this.kind.asString(), buf.len, buf[0..@min(buf.len, 16)], if (buf.len > 16) "..." else "" }); - io.buf.appendSlice(buf) catch bun.outOfMemory(); - return Maybe(usize).initResult(buf.len); - }, - .arraybuf => { - if (io.arraybuf.i >= io.arraybuf.buf.array_buffer.byte_len) { - // TODO is it correct to return an error here? is this error the correct one to return? - return Maybe(usize).initErr(Syscall.Error.fromCode(bun.C.E.NOSPC, .write)); - } + var io: *BuiltinIO.Output = &@field(this, @tagName(io_kind)); - const len = buf.len; - if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) { - // std.ArrayList(comptime T: type) - } - const write_len = if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) - io.arraybuf.buf.array_buffer.byte_len - io.arraybuf.i - else - len; - - const slice = io.arraybuf.buf.slice()[io.arraybuf.i .. io.arraybuf.i + write_len]; - @memcpy(slice, buf[0..write_len]); - io.arraybuf.i +|= @truncate(write_len); - log("{s} write to arraybuf {d}\n", .{ this.kind.asString(), write_len }); - return Maybe(usize).initResult(write_len); - }, - .blob, .ignore => return Maybe(usize).initResult(buf.len), - } + switch (io.*) { + .fd => @panic("writeNoIO can't write to a file descriptor"), + .buf => { + log("{s} write to buf len={d} str={s}{s}\n", .{ this.kind.asString(), buf.len, buf[0..@min(buf.len, 16)], if (buf.len > 16) "..." else "" }); + io.buf.appendSlice(buf) catch bun.outOfMemory(); + return buf.len; + }, + .arraybuf => { + if (io.arraybuf.i >= io.arraybuf.buf.array_buffer.byte_len) { + // TODO is it correct to return an error here? is this error the correct one to return? + // return Maybe(usize).initErr(Syscall.Error.fromCode(bun.C.E.NOSPC, .write)); + @panic("TODO shell: forgot this"); + } + + const len = buf.len; + if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) { + // std.ArrayList(comptime T: type) + } + const write_len = if (io.arraybuf.i + len > io.arraybuf.buf.array_buffer.byte_len) + io.arraybuf.buf.array_buffer.byte_len - io.arraybuf.i + else + len; + + const slice = io.arraybuf.buf.slice()[io.arraybuf.i .. io.arraybuf.i + write_len]; + @memcpy(slice, buf[0..write_len]); + io.arraybuf.i +|= @truncate(write_len); + log("{s} write to arraybuf {d}\n", .{ this.kind.asString(), write_len }); + return write_len; + }, + .blob, .ignore => return buf.len, } + } - /// Error messages formatted to match bash - fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: Syscall.Error) []const u8 { - return switch (err.getErrno()) { + /// Error messages formatted to match bash + fn taskErrorToString(this: *Builtin, comptime kind: Kind, err: anytype) []const u8 { + switch (@TypeOf(err)) { + Syscall.Error => return switch (err.getErrno()) { bun.C.E.NOENT => this.fmtErrorArena(kind, "{s}: No such file or directory\n", .{err.path}), bun.C.E.NAMETOOLONG => this.fmtErrorArena(kind, "{s}: File name too long\n", .{err.path}), bun.C.E.ISDIR => this.fmtErrorArena(kind, "{s}: is a directory\n", .{err.path}), bun.C.E.NOTEMPTY => this.fmtErrorArena(kind, "{s}: Directory not empty\n", .{err.path}), - else => err.toSystemError().message.byteSlice(), - }; - } - - pub fn ioAllClosed(this: *Builtin) bool { - return this.stdin.isClosed() and this.stdout.isClosed() and this.stderr.isClosed(); - } - - pub fn fmtErrorArena(this: *Builtin, comptime kind: ?Kind, comptime fmt_: []const u8, args: anytype) []u8 { - const cmd_str = comptime if (kind) |k| k.asString() ++ ": " else ""; - const fmt = cmd_str ++ fmt_; - return std.fmt.allocPrint(this.arena.allocator(), fmt, args) catch bun.outOfMemory(); + else => this.fmtErrorArena(kind, "{s}\n", .{err.toSystemError().message.byteSlice()}), + }, + JSC.SystemError => { + if (err.path.length() == 0) return this.fmtErrorArena(kind, "{s}\n", .{err.message.byteSlice()}); + return this.fmtErrorArena(kind, "{s}: {s}\n", .{ err.message.byteSlice(), err.path }); + }, + bun.shell.ShellErr => return switch (err) { + .sys => this.taskErrorToString(kind, err.sys), + .custom => this.fmtErrorArena(kind, "{s}\n", .{err.custom}), + .invalid_arguments => this.fmtErrorArena(kind, "{s}\n", .{err.invalid_arguments.val}), + .todo => this.fmtErrorArena(kind, "{s}\n", .{err.todo}), + }, + else => @compileError("Bad type: " ++ @typeName(err)), } + } - pub const Export = struct { - bltn: *Builtin, - print_state: ?struct { - bufwriter: BufferedWriter, - err: ?Syscall.Error = null, + // pub fn ioAllClosed(this: *Builtin) bool { + // return this.stdin.isClosed() and this.stdout.isClosed() and this.stderr.isClosed(); + // } - pub fn isDone(this: *@This()) bool { - return this.err != null or this.bufwriter.written >= this.bufwriter.remain.len; - } - } = null, + pub fn fmtErrorArena(this: *Builtin, comptime kind: ?Kind, comptime fmt_: []const u8, args: anytype) []u8 { + const cmd_str = comptime if (kind) |k| k.asString() ++ ": " else ""; + const fmt = cmd_str ++ fmt_; + return std.fmt.allocPrint(this.arena.allocator(), fmt, args) catch bun.outOfMemory(); + } - const Entry = struct { - key: EnvStr, - value: EnvStr, + pub const Cat = struct { + const print = bun.Output.scoped(.ShellCat, false); - pub fn compare(context: void, this: @This(), other: @This()) bool { - return bun.strings.cmpStringsAsc(context, this.key.slice(), other.key.slice()); - } - }; + bltn: *Builtin, + opts: Opts = .{}, + state: union(enum) { + idle, + exec_stdin: struct { + in_done: bool = false, + out_done: bool = false, + chunks_queued: usize = 0, + chunks_done: usize = 0, + errno: ExitCode = 0, + }, + exec_filepath_args: struct { + args: []const [*:0]const u8, + idx: usize = 0, + reader: ?*IOReader = null, + chunks_queued: usize = 0, + chunks_done: usize = 0, + out_done: bool = false, + in_done: bool = false, - pub fn writeOutput(this: *Export, comptime io_kind: @Type(.EnumLiteral), buf: []const u8) Maybe(void) { - if (!this.bltn.stdout.needsIO()) { - switch (this.bltn.writeNoIO(io_kind, buf)) { - .err => |e| { - this.bltn.exit_code = e.errno; - return Maybe(void).initErr(e); - }, - .result => |written| { - if (comptime bun.Environment.allow_assert) std.debug.assert(written == buf.len); - }, - } - this.bltn.done(0); - return Maybe(void).success; + pub fn deinit(this: *@This()) void { + if (this.reader) |r| r.deref(); } + }, + waiting_write_err, + done, + } = .idle, - this.print_state = .{ - .bufwriter = BufferedWriter{ - .remain = buf, - .fd = if (comptime io_kind == .stdout) this.bltn.stdout.expectFd() else this.bltn.stderr.expectFd(), - .parent = BufferedWriter.ParentPtr{ .ptr = BufferedWriter.ParentPtr.Repr.init(this) }, - .bytelist = this.bltn.stdBufferedBytelist(io_kind), - }, - }; - this.print_state.?.bufwriter.writeIfPossible(false); + pub fn writeFailingError(this: *Cat, buf: []const u8, exit_code: ExitCode) Maybe(void) { + if (this.bltn.stderr.needsIO()) { + this.state = .waiting_write_err; + this.bltn.stderr.enqueue(this, buf); return Maybe(void).success; } - pub fn onBufferedWriterDone(this: *Export, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.print_state != null); - } - - this.print_state.?.err = e; - const exit_code: ExitCode = if (e != null) e.?.errno else 0; - this.bltn.done(exit_code); - } + _ = this.bltn.writeNoIO(.stderr, buf); - pub fn start(this: *Export) Maybe(void) { - const args = this.bltn.argsSlice(); + this.bltn.done(exit_code); + return Maybe(void).success; + } - // Calling `export` with no arguments prints all exported variables lexigraphically ordered - if (args.len == 0) { - var arena = this.bltn.arena; + pub fn start(this: *Cat) Maybe(void) { + const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) { + .ok => |filepath_args| filepath_args, + .err => |e| { + const buf = switch (e) { + .illegal_option => |opt_str| this.bltn.fmtErrorArena(.cat, "illegal option -- {s}\n", .{opt_str}), + .show_usage => Builtin.Kind.cat.usageString(), + .unsupported => |unsupported| this.bltn.fmtErrorArena(.cat, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), + }; - var keys = std.ArrayList(Entry).init(arena.allocator()); - var iter = this.bltn.export_env.iterator(); - while (iter.next()) |entry| { - keys.append(.{ - .key = entry.key_ptr.*, - .value = entry.value_ptr.*, - }) catch bun.outOfMemory(); - } + _ = this.writeFailingError(buf, 1); + return Maybe(void).success; + }, + }; - std.mem.sort(Entry, keys.items[0..], {}, Entry.compare); + const should_read_from_stdin = filepath_args == null or filepath_args.?.len == 0; - const len = brk: { - var len: usize = 0; - for (keys.items) |entry| { - len += std.fmt.count("{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }); - } - break :brk len; - }; - var buf = arena.allocator().alloc(u8, len) catch bun.outOfMemory(); - { - var i: usize = 0; - for (keys.items) |entry| { - const written_slice = std.fmt.bufPrint(buf[i..], "{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }) catch @panic("This should not happen"); - i += written_slice.len; - } - } + if (should_read_from_stdin) { + this.state = .{ + .exec_stdin = .{ + // .in_done = !this.bltn.stdin.needsIO(), + // .out_done = !this.bltn.stdout.needsIO(), + }, + }; + } else { + this.state = .{ + .exec_filepath_args = .{ + .args = filepath_args.?, + // .in_done = !this.bltn.stdin.needsIO(), + // .out_done = !this.bltn.stdout.needsIO(), + }, + }; + } - if (!this.bltn.stdout.needsIO()) { - switch (this.bltn.writeNoIO(.stdout, buf)) { - .err => |e| { - this.bltn.exit_code = e.errno; - return Maybe(void).initErr(e); - }, - .result => |written| { - if (comptime bun.Environment.allow_assert) std.debug.assert(written == buf.len); - }, + _ = this.next(); + + return Maybe(void).success; + } + + pub fn next(this: *Cat) void { + switch (this.state) { + .idle => @panic("Invalid state"), + .exec_stdin => { + if (!this.bltn.stdin.needsIO()) { + this.state.exec_stdin.in_done = true; + const buf = this.bltn.readStdinNoIO(); + if (!this.bltn.stdout.needsIO()) { + _ = this.bltn.writeNoIO(.stdout, buf); + this.bltn.done(0); + return; } - this.bltn.done(0); - return Maybe(void).success; + this.bltn.stdout.enqueue(this, buf); + return; + } + this.bltn.stdin.fd.addReader(this); + this.bltn.stdin.fd.start(); + return; + }, + .exec_filepath_args => { + var exec = &this.state.exec_filepath_args; + if (exec.idx >= exec.args.len) { + exec.deinit(); + return this.bltn.done(0); } - if (comptime bun.Environment.allow_assert) {} + if (exec.reader) |r| r.deref(); - this.print_state = .{ - .bufwriter = BufferedWriter{ - .remain = buf, - .fd = this.bltn.stdout.expectFd(), - .parent = BufferedWriter.ParentPtr{ .ptr = BufferedWriter.ParentPtr.Repr.init(this) }, - .bytelist = this.bltn.stdBufferedBytelist(.stdout), + const arg = std.mem.span(exec.args[exec.idx]); + exec.idx += 1; + const dir = this.bltn.parentCmd().base.shell.cwd_fd; + const fd = switch (ShellSyscall.openat(dir, arg, os.O.RDONLY, 0)) { + .result => |fd| fd, + .err => |e| { + const buf = this.bltn.taskErrorToString(.cat, e); + _ = this.writeFailingError(buf, 1); + exec.deinit(); + return; }, }; - this.print_state.?.bufwriter.writeIfPossible(false); - - // if (this.print_state.?.isDone()) { - // if (this.print_state.?.bufwriter.err) |e| { - // this.bltn.exit_code = e.errno; - // return Maybe(void).initErr(e); - // } - // this.bltn.exit_code = 0; - // return Maybe(void).success; - // } - - return Maybe(void).success; - } + const reader = IOReader.init(fd, this.bltn.eventLoop()); + exec.chunks_done = 0; + exec.chunks_queued = 0; + exec.reader = reader; + exec.reader.?.addReader(this); + exec.reader.?.start(); + }, + .waiting_write_err => return, + .done => this.bltn.done(0), + } + } - for (args) |arg_raw| { - const arg_sentinel = arg_raw[0..std.mem.len(arg_raw) :0]; - const arg = arg_sentinel[0..arg_sentinel.len]; - if (arg.len == 0) continue; - - const eqsign_idx = std.mem.indexOfScalar(u8, arg, '=') orelse { - if (!shell.isValidVarName(arg)) { - const buf = this.bltn.fmtErrorArena(.@"export", "`{s}`: not a valid identifier", .{arg}); - return this.writeOutput(.stderr, buf); + pub fn onIOWriterChunk(this: *Cat, err: ?JSC.SystemError) void { + print("onIOWriterChunk(0x{x}, {s}, had_err={any})", .{ @intFromPtr(this), @tagName(this.state), err != null }); + // Writing to stdout errored, cancel everything and write error + if (err) |e| { + defer e.deref(); + switch (this.state) { + .exec_stdin => { + this.state.exec_stdin.out_done = true; + // Cancel reader if needed + if (!this.state.exec_stdin.in_done) { + if (this.bltn.stdin.needsIO()) { + this.bltn.stdin.fd.removeReader(this); + } + this.state.exec_stdin.in_done = true; } - this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(arg), EnvStr.initSlice(""), .exported); - continue; - }; - - const label = arg[0..eqsign_idx]; - const value = arg_sentinel[eqsign_idx + 1 .. :0]; - this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(label), EnvStr.initSlice(value), .exported); + this.bltn.done(e.getErrno()); + }, + .exec_filepath_args => { + var exec = &this.state.exec_filepath_args; + if (exec.reader) |r| { + r.removeReader(this); + } + exec.deinit(); + this.bltn.done(e.getErrno()); + }, + .waiting_write_err => this.bltn.done(e.getErrno()), + else => @panic("Invalid state"), } + return; + } - this.bltn.done(0); - return Maybe(void).success; + switch (this.state) { + .exec_stdin => { + this.state.exec_stdin.chunks_done += 1; + if (this.state.exec_stdin.in_done and this.state.exec_stdin.chunks_done >= this.state.exec_stdin.chunks_queued) { + this.bltn.done(0); + return; + } + // Need to wait for more chunks to be written + }, + .exec_filepath_args => { + this.state.exec_filepath_args.chunks_done += 1; + if (this.state.exec_filepath_args.in_done) { + this.next(); + return; + } + // Wait for reader to be done + return; + }, + .waiting_write_err => this.bltn.done(1), + else => @panic("Invalid state"), } + } - pub fn deinit(this: *Export) void { - log("({s}) deinit", .{@tagName(.@"export")}); - _ = this; + pub fn onIOReaderChunk(this: *Cat, chunk: []const u8) ReadChunkAction { + print("onIOReaderChunk(0x{x}, {s}, chunk_len={d})", .{ @intFromPtr(this), @tagName(this.state), chunk.len }); + switch (this.state) { + .exec_stdin => { + // out_done should only be done if reader is done (impossible since we just read a chunk) + // or it errored (also impossible since that removes us from the reader) + std.debug.assert(!this.state.exec_stdin.out_done); + if (this.bltn.stdout.needsIO()) { + this.state.exec_stdin.chunks_queued += 1; + this.bltn.stdout.enqueue(this, chunk); + return .cont; + } + _ = this.bltn.writeNoIO(.stdout, chunk); + }, + .exec_filepath_args => { + if (this.bltn.stdout.needsIO()) { + this.state.exec_filepath_args.chunks_queued += 1; + this.bltn.stdout.enqueue(this, chunk); + return .cont; + } + _ = this.bltn.writeNoIO(.stdout, chunk); + }, + else => @panic("Invalid state"), } - }; + return .cont; + } - pub const Echo = struct { - bltn: *Builtin, + pub fn onIOReaderDone(this: *Cat, err: ?JSC.SystemError) void { + const errno: ExitCode = if (err) |e| brk: { + defer e.deref(); + break :brk @as(ExitCode, @intCast(@intFromEnum(e.getErrno()))); + } else 0; + print("onIOReaderDone(0x{x}, {s}, errno={d})", .{ @intFromPtr(this), @tagName(this.state), errno }); - /// Should be allocated with the arena from Builtin - output: std.ArrayList(u8), + switch (this.state) { + .exec_stdin => { + this.state.exec_stdin.errno = errno; + this.state.exec_stdin.in_done = true; + if (errno != 0) { + if (this.state.exec_stdin.out_done or !this.bltn.stdout.needsIO()) { + this.bltn.done(errno); + return; + } + this.bltn.stdout.fd.writer.cancelChunks(this); + return; + } + if (this.state.exec_stdin.out_done or !this.bltn.stdout.needsIO()) { + this.bltn.done(0); + } + }, + .exec_filepath_args => { + this.state.exec_filepath_args.in_done = true; + if (errno != 0) { + if (this.state.exec_filepath_args.out_done or !this.bltn.stdout.needsIO()) { + this.state.exec_filepath_args.deinit(); + this.bltn.done(errno); + return; + } + this.bltn.stdout.fd.writer.cancelChunks(this); + return; + } + if (this.state.exec_filepath_args.out_done or (this.state.exec_filepath_args.chunks_done >= this.state.exec_filepath_args.chunks_queued) or !this.bltn.stdout.needsIO()) { + this.next(); + } + }, + .done, .waiting_write_err, .idle => {}, + } + } - io_write_state: ?BufferedWriter = null, + pub fn deinit(this: *Cat) void { + _ = this; // autofix + } - state: union(enum) { - idle, - waiting, - done, - err: Syscall.Error, - } = .idle, - - pub fn start(this: *Echo) Maybe(void) { - const args = this.bltn.argsSlice(); - - const args_len = args.len; - for (args, 0..) |arg, i| { - const len = std.mem.len(arg); - this.output.appendSlice(arg[0..len]) catch bun.outOfMemory(); - if (i < args_len - 1) { - this.output.append(' ') catch bun.outOfMemory(); - } - } + const Opts = struct { + /// -b + /// + /// Number the non-blank output lines, starting at 1. + number_nonblank: bool = false, - this.output.append('\n') catch bun.outOfMemory(); + /// -e + /// + /// Display non-printing characters and display a dollar sign ($) at the end of each line. + show_ends: bool = false, - if (!this.bltn.stdout.needsIO()) { - switch (this.bltn.writeNoIO(.stdout, this.output.items[0..])) { - .err => |e| { - this.state.err = e; - return Maybe(void).initErr(e); - }, - .result => {}, - } + /// -n + /// + /// Number the output lines, starting at 1. + number_all: bool = false, - this.state = .done; - this.bltn.done(0); - return Maybe(void).success; - } + /// -s + /// + /// Squeeze multiple adjacent empty lines, causing the output to be single spaced. + squeeze_blank: bool = false, - this.io_write_state = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = this.output.items[0..], - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), - }; - this.state = .waiting; - this.io_write_state.?.writeIfPossible(false); - return Maybe(void).success; - } + /// -t + /// + /// Display non-printing characters and display tab characters as ^I at the end of each line. + show_tabs: bool = false, - pub fn onBufferedWriterDone(this: *Echo, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.io_write_state != null and this.state == .waiting); - } + /// -u + /// + /// Disable output buffering. + disable_output_buffering: bool = false, - if (e != null) { - this.state = .{ .err = e.? }; - this.bltn.done(e.?.errno); - return; - } + /// -v + /// + /// Displays non-printing characters so they are visible. + show_nonprinting: bool = false, - this.state = .done; - this.bltn.done(0); + const Parse = FlagParser(*@This()); + + pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) { + return Parse.parseFlags(opts, args); } - pub fn deinit(this: *Echo) void { - log("({s}) deinit", .{@tagName(.echo)}); - _ = this; + pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult { + _ = this; // autofix + _ = flag; + return null; + } + + fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult { + _ = this; // autofix + switch (char) { + 'b' => { + return .{ .unsupported = unsupportedFlag("-b") }; + }, + 'e' => { + return .{ .unsupported = unsupportedFlag("-e") }; + }, + 'n' => { + return .{ .unsupported = unsupportedFlag("-n") }; + }, + 's' => { + return .{ .unsupported = unsupportedFlag("-s") }; + }, + 't' => { + return .{ .unsupported = unsupportedFlag("-t") }; + }, + 'u' => { + return .{ .unsupported = unsupportedFlag("-u") }; + }, + 'v' => { + return .{ .unsupported = unsupportedFlag("-v") }; + }, + else => { + return .{ .illegal_option = smallflags[1 + i ..] }; + }, + } + + return null; } }; + }; - /// 1 arg => returns absolute path of the arg (not found becomes exit code 1) - /// N args => returns absolute path of each separated by newline, if any path is not found, exit code becomes 1, but continues execution until all args are processed - pub const Which = struct { - bltn: *Builtin, + pub const Touch = struct { + bltn: *Builtin, + opts: Opts = .{}, + state: union(enum) { + idle, + exec: struct { + started: bool = false, + tasks_count: usize = 0, + tasks_done: usize = 0, + output_done: usize = 0, + output_waiting: usize = 0, + started_output_queue: bool = false, + args: []const [*:0]const u8, + err: ?JSC.SystemError = null, + }, + waiting_write_err, + done, + } = .idle, - state: union(enum) { - idle, - one_arg: struct { - writer: BufferedWriter, + pub fn deinit(this: *Touch) void { + _ = this; + } + + pub fn start(this: *Touch) Maybe(void) { + const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) { + .ok => |filepath_args| filepath_args, + .err => |e| { + const buf = switch (e) { + .illegal_option => |opt_str| this.bltn.fmtErrorArena(.touch, "illegal option -- {s}\n", .{opt_str}), + .show_usage => Builtin.Kind.touch.usageString(), + .unsupported => |unsupported| this.bltn.fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), + }; + + _ = this.writeFailingError(buf, 1); + return Maybe(void).success; }, - multi_args: struct { - args_slice: []const [*:0]const u8, - arg_idx: usize, - had_not_found: bool = false, - state: union(enum) { - none, - waiting_write: BufferedWriter, - }, + } orelse { + _ = this.writeFailingError(Builtin.Kind.touch.usageString(), 1); + return Maybe(void).success; + }; + + this.state = .{ + .exec = .{ + .args = filepath_args, }, - done, - err: Syscall.Error, - } = .idle, - - pub fn start(this: *Which) Maybe(void) { - const args = this.bltn.argsSlice(); - if (args.len == 0) { - if (!this.bltn.stdout.needsIO()) { - switch (this.bltn.writeNoIO(.stdout, "\n")) { - .err => |e| { - return Maybe(void).initErr(e); - }, - .result => {}, + }; + + _ = this.next(); + + return Maybe(void).success; + } + + pub fn next(this: *Touch) void { + switch (this.state) { + .idle => @panic("Invalid state"), + .exec => { + var exec = &this.state.exec; + if (exec.started) { + if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) { + const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; + this.state = .done; + this.bltn.done(exit_code); + return; } - this.bltn.done(1); - return Maybe(void).success; + return; } - this.state = .{ - .one_arg = .{ - .writer = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = "\n", - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), - }, - }, - }; - this.state.one_arg.writer.writeIfPossible(false); - return Maybe(void).success; - } - if (!this.bltn.stdout.needsIO()) { - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); - var had_not_found = false; - for (args) |arg_raw| { - const arg = arg_raw[0..std.mem.len(arg_raw)]; - const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { - had_not_found = true; - const buf = this.bltn.fmtErrorArena(.which, "{s} not found\n", .{arg}); - switch (this.bltn.writeNoIO(.stdout, buf)) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } - continue; - }; + exec.started = true; + exec.tasks_count = exec.args.len; - switch (this.bltn.writeNoIO(.stdout, resolved)) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } + for (exec.args) |dir_to_mk_| { + const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0]; + var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn.parentCmd().base.shell.cwdZ()); + task.schedule(); } - this.bltn.done(@intFromBool(had_not_found)); - return Maybe(void).success; - } + }, + .waiting_write_err => return, + .done => this.bltn.done(0), + } + } - this.state = .{ - .multi_args = .{ - .args_slice = args, - .arg_idx = 0, - .state = .none, - }, - }; - this.next(); - return Maybe(void).success; + pub fn onIOWriterChunk(this: *Touch, e: ?JSC.SystemError) void { + if (this.state == .waiting_write_err) { + // if (e) |err| return this.bltn.done(1); + return this.bltn.done(1); } - pub fn next(this: *Which) void { - var multiargs = &this.state.multi_args; - if (multiargs.arg_idx >= multiargs.args_slice.len) { - // Done - this.bltn.done(@intFromBool(multiargs.had_not_found)); - return; - } + if (e) |err| err.deref(); + + this.next(); + } - const arg_raw = multiargs.args_slice[multiargs.arg_idx]; - const arg = arg_raw[0..std.mem.len(arg_raw)]; + pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Maybe(void) { + if (this.bltn.stderr.needsIO()) { + this.state = .waiting_write_err; + this.bltn.stderr.enqueue(this, buf); + return Maybe(void).success; + } - var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); + _ = this.bltn.writeNoIO(.stderr, buf); + // if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| { + // return .{ .err = e }; + // } - const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { - const buf = this.bltn.fmtErrorArena(null, "{s} not found\n", .{arg}); - multiargs.had_not_found = true; - multiargs.state = .{ - .waiting_write = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), - }, - }; - multiargs.state.waiting_write.writeIfPossible(false); - // yield execution - return; - }; + this.bltn.done(exit_code); + return Maybe(void).success; + } - const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{resolved}); - multiargs.state = .{ - .waiting_write = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), - }, - }; - multiargs.state.waiting_write.writeIfPossible(false); + pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void { + defer bun.default_allocator.destroy(task); + this.state.exec.tasks_done += 1; + const err = task.err; + + if (err) |e| { + const output_task: *ShellTouchOutputTask = bun.new(ShellTouchOutputTask, .{ + .parent = this, + .output = .{ .arrlist = .{} }, + .state = .waiting_write_err, + }); + const error_string = this.bltn.taskErrorToString(.touch, e); + this.state.exec.err = e; + output_task.start(error_string); return; } - fn argComplete(this: *Which) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .multi_args and this.state.multi_args.state == .waiting_write); - } + this.next(); + } - this.state.multi_args.arg_idx += 1; - this.state.multi_args.state = .none; - this.next(); - } + pub const ShellTouchOutputTask = OutputTask(Touch, .{ + .writeErr = ShellTouchOutputTaskVTable.writeErr, + .onWriteErr = ShellTouchOutputTaskVTable.onWriteErr, + .writeOut = ShellTouchOutputTaskVTable.writeOut, + .onWriteOut = ShellTouchOutputTaskVTable.onWriteOut, + .onDone = ShellTouchOutputTaskVTable.onDone, + }); - pub fn onBufferedWriterDone(this: *Which, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .one_arg or - (this.state == .multi_args and this.state.multi_args.state == .waiting_write)); + const ShellTouchOutputTaskVTable = struct { + pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) CoroutineResult { + if (this.bltn.stderr.needsIO()) { + this.state.exec.output_waiting += 1; + this.bltn.stderr.enqueue(childptr, errbuf); + return .yield; } + _ = this.bltn.writeNoIO(.stderr, errbuf); + return .cont; + } - if (e != null) { - this.state = .{ .err = e.? }; - this.bltn.done(e.?.errno); - return; - } + pub fn onWriteErr(this: *Touch) void { + this.state.exec.output_done += 1; + } - if (this.state == .one_arg) { - // Calling which with on arguments returns exit code 1 - this.bltn.done(1); - return; + pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) CoroutineResult { + if (this.bltn.stdout.needsIO()) { + this.state.exec.output_waiting += 1; + const slice = output.slice(); + log("THE SLICE: {d} {s}", .{ slice.len, slice }); + this.bltn.stdout.enqueue(childptr, slice); + return .yield; } + _ = this.bltn.writeNoIO(.stdout, output.slice()); + return .cont; + } - this.argComplete(); + pub fn onWriteOut(this: *Touch) void { + this.state.exec.output_done += 1; } - pub fn deinit(this: *Which) void { - log("({s}) deinit", .{@tagName(.which)}); - _ = this; + pub fn onDone(this: *Touch) void { + this.next(); } }; - /// Some additional behaviour beyond basic `cd `: - /// - `cd` by itself or `cd ~` will always put the user in their home directory. - /// - `cd ~username` will put the user in the home directory of the specified user - /// - `cd -` will put the user in the previous directory - pub const Cd = struct { - bltn: *Builtin, - state: union(enum) { - idle, - waiting_write_stderr: struct { - buffered_writer: BufferedWriter, - }, - done, - err: Syscall.Error, - } = .idle, + pub const ShellTouchTask = struct { + touch: *Touch, - fn writeStderrNonBlocking(this: *Cd, buf: []u8) void { - this.state = .{ - .waiting_write_stderr = .{ - .buffered_writer = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }, - }; - this.state.waiting_write_stderr.buffered_writer.writeIfPossible(false); - } + opts: Opts, + filepath: [:0]const u8, + cwd_path: [:0]const u8, - pub fn start(this: *Cd) Maybe(void) { - const args = this.bltn.argsSlice(); - if (args.len > 1) { - const buf = this.bltn.fmtErrorArena(.cd, "too many arguments", .{}); - this.writeStderrNonBlocking(buf); - // yield execution - return Maybe(void).success; - } + err: ?JSC.SystemError = null, + task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool }, + event_loop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, - const first_arg = args[0][0..std.mem.len(args[0]) :0]; - switch (first_arg[0]) { - '-' => { - switch (this.bltn.parentCmd().base.shell.changePrevCwd(this.bltn.parentCmd().base.interpreter)) { - .result => {}, - .err => |err| { - return this.handleChangeCwdErr(err, this.bltn.parentCmd().base.shell.prevCwdZ()); - }, - } - }, - '~' => { - const homedir = this.bltn.parentCmd().base.shell.getHomedir(); - homedir.deref(); - switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, homedir.slice())) { - .result => {}, - .err => |err| return this.handleChangeCwdErr(err, homedir.slice()), - } - }, - else => { - switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, first_arg)) { - .result => {}, - .err => |err| return this.handleChangeCwdErr(err, first_arg), - } - }, + const print = bun.Output.scoped(.ShellTouchTask, false); + + pub fn deinit(this: *ShellTouchTask) void { + if (this.err) |e| { + e.deref(); } - this.bltn.done(0); - return Maybe(void).success; + bun.default_allocator.destroy(this); } - fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe(void) { - const errno: usize = @intCast(err.errno); - - switch (errno) { - @as(usize, @intFromEnum(bun.C.E.NOTDIR)) => { - const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}", .{new_cwd_}); - if (!this.bltn.stderr.needsIO()) { - switch (this.bltn.writeNoIO(.stderr, buf)) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } - this.state = .done; - this.bltn.done(1); - // yield execution - return Maybe(void).success; - } - - this.writeStderrNonBlocking(buf); - return Maybe(void).success; - }, - @as(usize, @intFromEnum(bun.C.E.NOENT)) => { - const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}", .{new_cwd_}); - if (!this.bltn.stderr.needsIO()) { - switch (this.bltn.writeNoIO(.stderr, buf)) { - .err => |e| return Maybe(void).initErr(e), - .result => {}, - } - this.state = .done; - this.bltn.done(1); - // yield execution - return Maybe(void).success; - } - - this.writeStderrNonBlocking(buf); - return Maybe(void).success; - }, - else => return Maybe(void).success, - } + pub fn create(touch: *Touch, opts: Opts, filepath: [:0]const u8, cwd_path: [:0]const u8) *ShellTouchTask { + const task = bun.default_allocator.create(ShellTouchTask) catch bun.outOfMemory(); + task.* = ShellTouchTask{ + .touch = touch, + .opts = opts, + .cwd_path = cwd_path, + .filepath = filepath, + .event_loop = touch.bltn.eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(touch.bltn.eventLoop()), + }; + return task; } - pub fn onBufferedWriterDone(this: *Cd, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .waiting_write_stderr); - } - - if (e != null) { - this.state = .{ .err = e.? }; - this.bltn.done(e.?.errno); - return; - } - - this.state = .done; - this.bltn.done(0); + pub fn schedule(this: *@This()) void { + print("schedule", .{}); + WorkPool.schedule(&this.task); } - pub fn deinit(this: *Cd) void { - log("({s}) deinit", .{@tagName(.cd)}); - _ = this; + pub fn runFromMainThread(this: *@This()) void { + print("runFromJS", .{}); + this.touch.onShellTouchTaskDone(this); } - }; - pub const Pwd = struct { - bltn: *Builtin, - state: union(enum) { - idle, - waiting_io: struct { - kind: enum { stdout, stderr }, - writer: BufferedWriter, - }, - err: Syscall.Error, - done, - } = .idle, - - pub fn start(this: *Pwd) Maybe(void) { - const args = this.bltn.argsSlice(); - if (args.len > 0) { - const msg = "pwd: too many arguments"; - if (this.bltn.stderr.needsIO()) { - this.state = .{ - .waiting_io = .{ - .kind = .stderr, - .writer = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = msg, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }, - }; - this.state.waiting_io.writer.writeIfPossible(false); - return Maybe(void).success; - } + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } - if (this.bltn.writeNoIO(.stderr, msg).asErr()) |e| { - return .{ .err = e }; - } + fn runFromThreadPool(task: *JSC.WorkPoolTask) void { + var this: *ShellTouchTask = @fieldParentPtr(ShellTouchTask, "task", task); - this.bltn.done(1); - return Maybe(void).success; - } + // We have to give an absolute path + const filepath: [:0]const u8 = brk: { + if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath; + const parts: []const []const u8 = &.{ + this.cwd_path[0..], + this.filepath[0..], + }; + break :brk ResolvePath.joinZ(parts, .auto); + }; - const cwd_str = this.bltn.parentCmd().base.shell.cwd(); - const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{cwd_str}); - if (this.bltn.stdout.needsIO()) { - this.state = .{ - .waiting_io = .{ - .kind = .stdout, - .writer = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), + var node_fs = JSC.Node.NodeFS{}; + const milliseconds: f64 = @floatFromInt(std.time.milliTimestamp()); + const atime: JSC.Node.TimeLike = if (bun.Environment.isWindows) milliseconds / 1000.0 else JSC.Node.TimeLike{ + .tv_sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)), + .tv_nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms), + }; + const mtime = atime; + const args = JSC.Node.Arguments.Utimes{ + .atime = atime, + .mtime = mtime, + .path = .{ .string = bun.PathString.init(filepath) }, + }; + if (node_fs.utimes(args, .callback).asErr()) |err| out: { + if (err.getErrno() == bun.C.E.NOENT) { + const perm = 0o664; + switch (Syscall.open(filepath, std.os.O.CREAT | std.os.O.WRONLY, perm)) { + .result => break :out, + .err => |e| { + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + break :out; }, - }, - }; - this.state.waiting_io.writer.writeIfPossible(false); - return Maybe(void).success; + } + } + this.err = err.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); } - if (this.bltn.writeNoIO(.stdout, buf).asErr()) |err| { - return .{ .err = err }; + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); } - - this.state = .done; - this.bltn.done(0); - return Maybe(void).success; } + }; - pub fn next(this: *Pwd) void { - while (!(this.state == .err or this.state == .done)) { - switch (this.state) { - .waiting_io => return, - .idle, .done, .err => unreachable, - } - } - - if (this.state == .done) { - this.bltn.done(0); - return; + const Opts = struct { + /// -a + /// + /// change only the access time + access_time_only: bool = false, + + /// -c, --no-create + /// + /// do not create any files + no_create: bool = false, + + /// -d, --date=STRING + /// + /// parse STRING and use it instead of current time + date: ?[]const u8 = null, + + /// -h, --no-dereference + /// + /// affect each symbolic link instead of any referenced file + /// (useful only on systems that can change the timestamps of a symlink) + no_dereference: bool = false, + + /// -m + /// + /// change only the modification time + modification_time_only: bool = false, + + /// -r, --reference=FILE + /// + /// use this file's times instead of current time + reference: ?[]const u8 = null, + + /// -t STAMP + /// + /// use [[CC]YY]MMDDhhmm[.ss] instead of current time + timestamp: ?[]const u8 = null, + + /// --time=WORD + /// + /// change the specified time: + /// WORD is access, atime, or use: equivalent to -a + /// WORD is modify or mtime: equivalent to -m + time: ?[]const u8 = null, + + const Parse = FlagParser(*@This()); + + pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) { + return Parse.parseFlags(opts, args); + } + + pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult { + _ = this; + if (bun.strings.eqlComptime(flag, "--no-create")) { + return .{ + .unsupported = unsupportedFlag("--no-create"), + }; } - if (this.state == .err) { - this.bltn.done(this.state.err.errno); - return; + if (bun.strings.eqlComptime(flag, "--date")) { + return .{ + .unsupported = unsupportedFlag("--date"), + }; } - } - pub fn onBufferedWriterDone(this: *Pwd, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .waiting_io); + if (bun.strings.eqlComptime(flag, "--reference")) { + return .{ + .unsupported = unsupportedFlag("--reference=FILE"), + }; } - if (e != null) { - this.state = .{ .err = e.? }; - this.next(); - return; + if (bun.strings.eqlComptime(flag, "--time")) { + return .{ + .unsupported = unsupportedFlag("--reference=FILE"), + }; } - this.state = .done; - - this.next(); + return null; } - pub fn deinit(this: *Pwd) void { + fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult { _ = this; + switch (char) { + 'a' => { + return .{ .unsupported = unsupportedFlag("-a") }; + }, + 'c' => { + return .{ .unsupported = unsupportedFlag("-c") }; + }, + 'd' => { + return .{ .unsupported = unsupportedFlag("-d") }; + }, + 'h' => { + return .{ .unsupported = unsupportedFlag("-h") }; + }, + 'm' => { + return .{ .unsupported = unsupportedFlag("-m") }; + }, + 'r' => { + return .{ .unsupported = unsupportedFlag("-r") }; + }, + 't' => { + return .{ .unsupported = unsupportedFlag("-t") }; + }, + else => { + return .{ .illegal_option = smallflags[1 + i ..] }; + }, + } + + return null; } }; + }; - pub const Ls = struct { - bltn: *Builtin, - opts: Opts = .{}, - - state: union(enum) { - idle, - exec: struct { - err: ?Syscall.Error = null, - task_count: std.atomic.Value(usize), - tasks_done: usize = 0, - output_queue: std.DoublyLinkedList(BlockingOutput) = .{}, - started_output_queue: bool = false, - }, - waiting_write_err: BufferedWriter, - done, - } = .idle, + pub const Mkdir = struct { + bltn: *Builtin, + opts: Opts = .{}, + state: union(enum) { + idle, + exec: struct { + started: bool = false, + tasks_count: usize = 0, + tasks_done: usize = 0, + output_waiting: u16 = 0, + output_done: u16 = 0, + args: []const [*:0]const u8, + err: ?JSC.SystemError = null, + }, + waiting_write_err, + done, + } = .idle, - const BlockingOutput = struct { - writer: BufferedWriter, - arr: std.ArrayList(u8), + pub fn onIOWriterChunk(this: *Mkdir, e: ?JSC.SystemError) void { + if (e) |err| err.deref(); - pub fn deinit(this: *BlockingOutput) void { - this.arr.deinit(); - } - }; + switch (this.state) { + .waiting_write_err => return this.bltn.done(1), + .exec => { + this.state.exec.output_done += 1; + }, + .idle, .done => @panic("Invalid state"), + } - pub fn start(this: *Ls) Maybe(void) { - this.next(); + this.next(); + } + pub fn writeFailingError(this: *Mkdir, buf: []const u8, exit_code: ExitCode) Maybe(void) { + if (this.bltn.stderr.needsIO()) { + this.state = .waiting_write_err; + this.bltn.stderr.enqueue(this, buf); return Maybe(void).success; } - pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Maybe(void) { - if (this.bltn.stderr.needsIO()) { - this.state = .{ - .waiting_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - this.state.waiting_write_err.writeIfPossible(false); - return Maybe(void).success; - } + _ = this.bltn.writeNoIO(.stderr, buf); + // if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| { + // return .{ .err = e }; + // } - if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| { - return .{ .err = e }; - } + this.bltn.done(exit_code); + return Maybe(void).success; + } + + pub fn start(this: *Mkdir) Maybe(void) { + const filepath_args = switch (this.opts.parse(this.bltn.argsSlice())) { + .ok => |filepath_args| filepath_args, + .err => |e| { + const buf = switch (e) { + .illegal_option => |opt_str| this.bltn.fmtErrorArena(.mkdir, "illegal option -- {s}\n", .{opt_str}), + .show_usage => Builtin.Kind.mkdir.usageString(), + .unsupported => |unsupported| this.bltn.fmtErrorArena(.mkdir, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}), + }; - this.bltn.done(exit_code); + _ = this.writeFailingError(buf, 1); + return Maybe(void).success; + }, + } orelse { + _ = this.writeFailingError(Builtin.Kind.mkdir.usageString(), 1); return Maybe(void).success; - } + }; - fn next(this: *Ls) void { - while (!(this.state == .done)) { - switch (this.state) { - .idle => { - // Will be null if called with no args, in which case we just run once with "." directory - const paths: ?[]const [*:0]const u8 = switch (this.parseOpts()) { - .ok => |paths| paths, - .err => |e| { - const buf = switch (e) { - .illegal_option => |opt_str| this.bltn.fmtErrorArena(.ls, "illegal option -- {s}\n", .{opt_str}), - .show_usage => Builtin.Kind.ls.usageString(), - }; - - _ = this.writeFailingError(buf, 1); - return; - }, - }; + this.state = .{ + .exec = .{ + .args = filepath_args, + }, + }; - const task_count = if (paths) |p| p.len else 1; + _ = this.next(); - this.state = .{ - .exec = .{ - .task_count = std.atomic.Value(usize).init(task_count), - }, - }; + return Maybe(void).success; + } - const cwd = this.bltn.cwd; - if (paths) |p| { - for (p) |path_raw| { - const path = path_raw[0..std.mem.len(path_raw) :0]; - var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, path, null); - task.schedule(); - } - } else { - var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, ".", null); - task.schedule(); - } - }, - .exec => { - // It's done - if (this.state.exec.tasks_done >= this.state.exec.task_count.load(.Monotonic) and this.state.exec.output_queue.len == 0) { - const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; - this.state = .done; - this.bltn.done(exit_code); - return; - } - return; - }, - .waiting_write_err => { + pub fn next(this: *Mkdir) void { + switch (this.state) { + .idle => @panic("Invalid state"), + .exec => { + var exec = &this.state.exec; + if (exec.started) { + if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) { + const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; + if (this.state.exec.err) |e| e.deref(); + this.state = .done; + this.bltn.done(exit_code); return; - }, - .done => unreachable, + } + return; } - } - this.bltn.done(0); - return; - } + exec.started = true; + exec.tasks_count = exec.args.len; - pub fn deinit(this: *Ls) void { - _ = this; // autofix + for (exec.args) |dir_to_mk_| { + const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0]; + var task = ShellMkdirTask.create(this, this.opts, dir_to_mk, this.bltn.parentCmd().base.shell.cwdZ()); + task.schedule(); + } + }, + .waiting_write_err => return, + .done => this.bltn.done(0), } + } + + pub fn onShellMkdirTaskDone(this: *Mkdir, task: *ShellMkdirTask) void { + defer bun.default_allocator.destroy(task); + this.state.exec.tasks_done += 1; + var output = task.takeOutput(); + const err = task.err; + const output_task: *ShellMkdirOutputTask = bun.new(ShellMkdirOutputTask, .{ + .parent = this, + .output = .{ .arrlist = output.moveToUnmanaged() }, + .state = .waiting_write_err, + }); - pub fn queueBlockingOutput(this: *Ls, bo: BlockingOutput) void { - _ = this.queueBlockingOutputImpl(bo, true); + if (err) |e| { + const error_string = this.bltn.taskErrorToString(.mkdir, e); + this.state.exec.err = e; + output_task.start(error_string); + return; } + output_task.start(null); + } - pub fn queueBlockingOutputImpl(this: *Ls, bo: BlockingOutput, do_run: bool) CoroutineResult { - const node = bun.default_allocator.create(std.DoublyLinkedList(BlockingOutput).Node) catch bun.outOfMemory(); - node.* = .{ - .data = bo, - }; - this.state.exec.output_queue.append(node); + pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{ + .writeErr = ShellMkdirOutputTaskVTable.writeErr, + .onWriteErr = ShellMkdirOutputTaskVTable.onWriteErr, + .writeOut = ShellMkdirOutputTaskVTable.writeOut, + .onWriteOut = ShellMkdirOutputTaskVTable.onWriteOut, + .onDone = ShellMkdirOutputTaskVTable.onDone, + }); - // Start it - if (this.state.exec.output_queue.len == 1 and do_run) { - // if (do_run and !this.state.exec.started_output_queue) { - this.state.exec.started_output_queue = true; - this.state.exec.output_queue.first.?.data.writer.writeIfPossible(false); + const ShellMkdirOutputTaskVTable = struct { + pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) CoroutineResult { + if (this.bltn.stderr.needsIO()) { + this.state.exec.output_waiting += 1; + this.bltn.stderr.enqueue(childptr, errbuf); return .yield; } + _ = this.bltn.writeNoIO(.stderr, errbuf); return .cont; } - fn scheduleBlockingOutput(this: *Ls) CoroutineResult { - if (this.state.exec.output_queue.len > 0) { - this.state.exec.output_queue.first.?.data.writer.writeIfPossible(false); + pub fn onWriteErr(this: *Mkdir) void { + this.state.exec.output_done += 1; + } + + pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) CoroutineResult { + if (this.bltn.stdout.needsIO()) { + this.state.exec.output_waiting += 1; + const slice = output.slice(); + log("THE SLICE: {d} {s}", .{ slice.len, slice }); + this.bltn.stdout.enqueue(childptr, slice); return .yield; } + _ = this.bltn.writeNoIO(.stdout, output.slice()); return .cont; } - pub fn onBufferedWriterDone(this: *Ls, e: ?Syscall.Error) void { - _ = e; // autofix - - if (this.state == .waiting_write_err) { - // if (e) |err| return this.bltn.done(1); - return this.bltn.done(1); - } - - var queue = &this.state.exec.output_queue; - var first = queue.popFirst().?; - defer { - first.data.deinit(); - bun.default_allocator.destroy(first); - } - if (first.next) |next_writer| { - next_writer.data.writer.writeIfPossible(false); - return; - } + pub fn onWriteOut(this: *Mkdir) void { + this.state.exec.output_done += 1; + } + pub fn onDone(this: *Mkdir) void { this.next(); } + }; - pub fn onAsyncTaskDone(this: *Ls, task_: *ShellLsTask) void { - this.state.exec.tasks_done += 1; - const output = task_.takeOutput(); - const err = task_.err; - task_.deinit(); - - // const need_to_write_to_stdout_with_io = output.items.len > 0 and this.bltn.stdout.needsIO(); - var queued: bool = false; - - // Check for error, print it, but still want to print task output - if (err) |e| { - const error_string = this.bltn.taskErrorToString(.ls, e); - this.state.exec.err = e; - - if (this.bltn.stderr.needsIO()) { - queued = true; - const blocking_output: BlockingOutput = .{ - .writer = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - .arr = std.ArrayList(u8).init(bun.default_allocator), - }; - _ = this.queueBlockingOutputImpl(blocking_output, false); - // if (!need_to_write_to_stdout_with_io) return; // yield execution - } else { - if (this.bltn.writeNoIO(.stderr, error_string).asErr()) |theerr| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(theerr)); - } - } - } + pub fn deinit(this: *Mkdir) void { + _ = this; + } - if (this.bltn.stdout.needsIO()) { - queued = true; - const blocking_output: BlockingOutput = .{ - .writer = BufferedWriter{ - .fd = this.bltn.stdout.expectFd(), - .remain = output.items[0..], - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stdout), - }, - .arr = output, - }; - _ = this.queueBlockingOutputImpl(blocking_output, false); - // if (this.state == .done) return; - // return this.next(); - } + pub const ShellMkdirTask = struct { + mkdir: *Mkdir, - if (queued) { - if (this.scheduleBlockingOutput() == .yield) return; - if (this.state == .done) return; - return this.next(); - } + opts: Opts, + filepath: [:0]const u8, + cwd_path: [:0]const u8, + created_directories: ArrayList(u8), - defer output.deinit(); + err: ?JSC.SystemError = null, + task: JSC.WorkPoolTask = .{ .callback = &runFromThreadPool }, + event_loop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, - if (this.bltn.writeNoIO(.stdout, output.items[0..]).asErr()) |e| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(e)); - return; - } + const print = bun.Output.scoped(.ShellMkdirTask, false); - return this.next(); + fn takeOutput(this: *ShellMkdirTask) ArrayList(u8) { + const out = this.created_directories; + this.created_directories = ArrayList(u8).init(bun.default_allocator); + return out; } - pub const ShellLsTask = struct { - const print = bun.Output.scoped(.ShellLsTask, false); - ls: *Ls, + pub fn create( + mkdir: *Mkdir, opts: Opts, + filepath: [:0]const u8, + cwd_path: [:0]const u8, + ) *ShellMkdirTask { + const task = bun.default_allocator.create(ShellMkdirTask) catch bun.outOfMemory(); + const evtloop = mkdir.bltn.parentCmd().base.eventLoop(); + task.* = ShellMkdirTask{ + .mkdir = mkdir, + .opts = opts, + .cwd_path = cwd_path, + .filepath = filepath, + .created_directories = ArrayList(u8).init(bun.default_allocator), + .event_loop = evtloop, + .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop), + }; + return task; + } - is_root: bool = true, - task_count: *std.atomic.Value(usize), + pub fn schedule(this: *@This()) void { + print("schedule", .{}); + WorkPool.schedule(&this.task); + } - cwd: bun.FileDescriptor, - /// Should be allocated with bun.default_allocator - path: [:0]const u8 = &[0:0]u8{}, - /// Should use bun.default_allocator - output: std.ArrayList(u8), - is_absolute: bool = false, - err: ?Syscall.Error = null, - result_kind: enum { file, dir, idk } = .idk, + pub fn runFromMainThread(this: *@This()) void { + print("runFromJS", .{}); + this.mkdir.onShellMkdirTaskDone(this); + } - event_loop: EventLoopRef, - concurrent_task: EventLoopTask = .{}, - task: JSC.WorkPoolTask = .{ - .callback = workPoolCallback, - }, + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } - pub fn schedule(this: *@This()) void { - JSC.WorkPool.schedule(&this.task); - } + fn runFromThreadPool(task: *JSC.WorkPoolTask) void { + var this: *ShellMkdirTask = @fieldParentPtr(ShellMkdirTask, "task", task); - pub fn create(ls: *Ls, opts: Opts, task_count: *std.atomic.Value(usize), cwd: bun.FileDescriptor, path: [:0]const u8, event_loop: ?EventLoopRef) *@This() { - const task = bun.default_allocator.create(@This()) catch bun.outOfMemory(); - task.* = @This(){ - .ls = ls, - .opts = opts, - .cwd = cwd, - .path = bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory(), - .output = std.ArrayList(u8).init(bun.default_allocator), - // .event_loop = event_loop orelse JSC.VirtualMachine.get().eventLoop(), - .event_loop = event_loop orelse event_loop_ref.get(), - .task_count = task_count, + // We have to give an absolute path to our mkdir + // implementation for it to work with cwd + const filepath: [:0]const u8 = brk: { + if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath; + const parts: []const []const u8 = &.{ + this.cwd_path[0..], + this.filepath[0..], }; - return task; - } - - pub fn enqueue(this: *@This(), path: [:0]const u8) void { - print("enqueue: {s}", .{path}); - const new_path = this.join( - bun.default_allocator, - &[_][]const u8{ - this.path[0..this.path.len], - path[0..path.len], - }, - this.is_absolute, - ); - - var subtask = @This().create(this.ls, this.opts, this.task_count, this.cwd, new_path, this.event_loop); - _ = this.task_count.fetchAdd(1, .Monotonic); - subtask.is_root = false; - subtask.schedule(); - } - - inline fn join(this: *@This(), alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 { - _ = this; // autofix - if (!is_absolute) { - // If relative paths enabled, stdlib join is preferred over - // ResolvePath.joinBuf because it doesn't try to normalize the path - return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory(); - } + break :brk ResolvePath.joinZ(parts, .auto); + }; - const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory(); + var node_fs = JSC.Node.NodeFS{}; + // Recursive + if (this.opts.parents) { + const args = JSC.Node.Arguments.Mkdir{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) }, + .recursive = true, + .always_return_none = true, + }; - return out; - } + var vtable = MkdirVerboseVTable{ .inner = this, .active = this.opts.verbose }; - pub fn run(this: *@This()) void { - const fd = switch (Syscall.openat(this.cwd, this.path, os.O.RDONLY | os.O.DIRECTORY, 0)) { + switch (node_fs.mkdirRecursiveImpl(args, .callback, *MkdirVerboseVTable, &vtable)) { + .result => {}, .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - this.err = this.errorWithPath(e, this.path); - }, - bun.C.E.NOTDIR => { - this.result_kind = .file; - this.addEntry(this.path); - }, - else => { - this.err = this.errorWithPath(e, this.path); - }, - } - return; + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + std.mem.doNotOptimizeAway(&node_fs); }, - .result => |fd| fd, + } + } else { + const args = JSC.Node.Arguments.Mkdir{ + .path = JSC.Node.PathLike{ .string = bun.PathString.init(filepath) }, + .recursive = false, + .always_return_none = true, }; - - defer { - _ = Syscall.close(fd); - print("run done", .{}); + switch (node_fs.mkdirNonRecursive(args, .callback)) { + .result => { + if (this.opts.verbose) { + this.created_directories.appendSlice(filepath[0..filepath.len]) catch bun.outOfMemory(); + this.created_directories.append('\n') catch bun.outOfMemory(); + } + }, + .err => |e| { + this.err = e.withPath(bun.default_allocator.dupe(u8, filepath) catch bun.outOfMemory()).toSystemError(); + std.mem.doNotOptimizeAway(&node_fs); + }, } + } - if (!this.opts.list_directories) { - if (!this.is_root) { - const writer = this.output.writer(); - std.fmt.format(writer, "{s}:\n", .{this.path}) catch bun.outOfMemory(); - } - - var iterator = DirIterator.iterate(fd.asDir(), .u8); - var entry = iterator.next(); + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } - while (switch (entry) { - .err => |e| { - this.err = this.errorWithPath(e, this.path); - return; - }, - .result => |ent| ent, - }) |current| : (entry = iterator.next()) { - this.addEntry(current.name.sliceAssumeZ()); - if (current.kind == .directory and this.opts.recursive) { - this.enqueue(current.name.sliceAssumeZ()); - } - } + const MkdirVerboseVTable = struct { + inner: *ShellMkdirTask, + active: bool, - return; + pub fn onCreateDir(vtable: *@This(), dirpath: bun.OSPathSliceZ) void { + if (!vtable.active) return; + if (bun.Environment.isWindows) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const str = bun.strings.fromWPath(&buf, dirpath[0..dirpath.len]); + vtable.inner.created_directories.appendSlice(str) catch bun.outOfMemory(); + vtable.inner.created_directories.append('\n') catch bun.outOfMemory(); + } else { + vtable.inner.created_directories.appendSlice(dirpath) catch bun.outOfMemory(); + vtable.inner.created_directories.append('\n') catch bun.outOfMemory(); } - - const writer = this.output.writer(); - std.fmt.format(writer, "{s}\n", .{this.path}) catch bun.outOfMemory(); return; } + }; + }; - fn shouldSkipEntry(this: *@This(), name: [:0]const u8) bool { - if (this.opts.show_all) return false; - if (this.opts.show_almost_all) { - if (bun.strings.eqlComptime(name[0..1], ".") or bun.strings.eqlComptime(name[0..2], "..")) return true; - } - return false; - } - - // TODO more complex output like multi-column - fn addEntry(this: *@This(), name: [:0]const u8) void { - const skip = this.shouldSkipEntry(name); - print("Entry: (skip={}) {s} :: {s}", .{ skip, this.path, name }); - if (skip) return; - this.output.ensureUnusedCapacity(name.len + 1) catch bun.outOfMemory(); - this.output.appendSlice(name) catch bun.outOfMemory(); - // FIXME TODO non ascii/utf-8 - this.output.append('\n') catch bun.outOfMemory(); - } - - fn errorWithPath(this: *@This(), err: Syscall.Error, path: [:0]const u8) Syscall.Error { - _ = this; - return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory()); - } + const Opts = struct { + /// -m, --mode + /// + /// set file mode (as in chmod), not a=rwx - umask + mode: ?u32 = null, - pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { - var this: *@This() = @fieldParentPtr(@This(), "task", task); - this.run(); - this.doneLogic(); - } + /// -p, --parents + /// + /// no error if existing, make parent directories as needed, + /// with their file modes unaffected by any -m option. + parents: bool = false, - fn doneLogic(this: *@This()) void { - print("Done", .{}); - if (comptime EventLoopKind == .js) { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } else { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, "runFromMainThreadMini")); - } + /// -v, --verbose + /// + /// print a message for each created directory + verbose: bool = false, - // if (this.parent) |parent| { - // _ = parent.children_done.fetchAdd(1, .Monotonic); - // if (parent.childrenAreDone()) parent.doneLogic(); - // } - } + const Parse = FlagParser(*@This()); - pub fn takeOutput(this: *@This()) std.ArrayList(u8) { - const ret = this.output; - this.output = std.ArrayList(u8).init(bun.default_allocator); - return ret; - } + pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) { + return Parse.parseFlags(opts, args); + } - pub fn runFromMainThread(this: *@This()) void { - print("runFromMainThread", .{}); - this.ls.onAsyncTaskDone(this); + pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult { + if (bun.strings.eqlComptime(flag, "--mode")) { + return .{ .unsupported = "--mode" }; + } else if (bun.strings.eqlComptime(flag, "--parents")) { + this.parents = true; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--vebose")) { + this.verbose = true; + return .continue_parsing; } - pub fn runFromMainThreadMini(this: *@This(), _: *void) void { - this.runFromMainThread(); - } + return null; + } - pub fn deinit(this: *@This()) void { - print("deinit", .{}); - bun.default_allocator.free(this.path); - this.output.deinit(); - bun.default_allocator.destroy(this); + fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult { + switch (char) { + 'm' => { + return .{ .unsupported = "-m " }; + }, + 'p' => { + this.parents = true; + }, + 'v' => { + this.verbose = true; + }, + else => { + return .{ .illegal_option = smallflags[1 + i ..] }; + }, } - }; - - const Opts = struct { - /// `-a`, `--all` - /// Do not ignore entries starting with . - show_all: bool = false, - - /// `-A`, `--almost-all` - /// Do not list implied . and .. - show_almost_all: bool = true, - - /// `--author` - /// With -l, print the author of each file - show_author: bool = false, - /// `-b`, `--escape` - /// Print C-style escapes for nongraphic characters - escape: bool = false, - - /// `--block-size=SIZE` - /// With -l, scale sizes by SIZE when printing them; e.g., '--block-size=M' - block_size: ?usize = null, - - /// `-B`, `--ignore-backups` - /// Do not list implied entries ending with ~ - ignore_backups: bool = false, - - /// `-c` - /// Sort by, and show, ctime (time of last change of file status information); affects sorting and display based on options - use_ctime: bool = false, + return null; + } + }; + }; - /// `-C` - /// List entries by columns - list_by_columns: bool = false, + pub const Export = struct { + bltn: *Builtin, + printing: bool = false, - /// `--color[=WHEN]` - /// Color the output; WHEN can be 'always', 'auto', or 'never' - color: ?[]const u8 = null, + const Entry = struct { + key: EnvStr, + value: EnvStr, - /// `-d`, `--directory` - /// List directories themselves, not their contents - list_directories: bool = false, + pub fn compare(context: void, this: @This(), other: @This()) bool { + return bun.strings.cmpStringsAsc(context, this.key.slice(), other.key.slice()); + } + }; - /// `-D`, `--dired` - /// Generate output designed for Emacs' dired mode - dired_mode: bool = false, + pub fn writeOutput(this: *Export, comptime io_kind: @Type(.EnumLiteral), comptime fmt: []const u8, args: anytype) Maybe(void) { + if (!this.bltn.stdout.needsIO()) { + const buf = this.bltn.fmtErrorArena(.@"export", fmt, args); + _ = this.bltn.writeNoIO(io_kind, buf); + this.bltn.done(0); + return Maybe(void).success; + } - /// `-f` - /// List all entries in directory order - unsorted: bool = false, + var output: *BuiltinIO.Output = &@field(this.bltn, @tagName(io_kind)); + this.printing = true; + output.enqueueFmtBltn(this, .@"export", fmt, args); + return Maybe(void).success; + } - /// `-F`, `--classify[=WHEN]` - /// Append indicator (one of */=>@|) to entries; WHEN can be 'always', 'auto', or 'never' - classify: ?[]const u8 = null, + pub fn onIOWriterChunk(this: *Export, e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.printing); + } - /// `--file-type` - /// Likewise, except do not append '*' - file_type: bool = false, + const exit_code: ExitCode = if (e != null) brk: { + defer e.?.deref(); + break :brk @intFromEnum(e.?.getErrno()); + } else 0; - /// `--format=WORD` - /// Specify format: 'across', 'commas', 'horizontal', 'long', 'single-column', 'verbose', 'vertical' - format: ?[]const u8 = null, + this.bltn.done(exit_code); + } - /// `--full-time` - /// Like -l --time-style=full-iso - full_time: bool = false, + pub fn start(this: *Export) Maybe(void) { + const args = this.bltn.argsSlice(); - /// `-g` - /// Like -l, but do not list owner - no_owner: bool = false, + // Calling `export` with no arguments prints all exported variables lexigraphically ordered + if (args.len == 0) { + var arena = this.bltn.arena; - /// `--group-directories-first` - /// Group directories before files - group_directories_first: bool = false, + var keys = std.ArrayList(Entry).init(arena.allocator()); + var iter = this.bltn.export_env.iterator(); + while (iter.next()) |entry| { + keys.append(.{ + .key = entry.key_ptr.*, + .value = entry.value_ptr.*, + }) catch bun.outOfMemory(); + } - /// `-G`, `--no-group` - /// In a long listing, don't print group names - no_group: bool = false, + std.mem.sort(Entry, keys.items[0..], {}, Entry.compare); - /// `-h`, `--human-readable` - /// With -l and -s, print sizes like 1K 234M 2G etc. - human_readable: bool = false, + const len = brk: { + var len: usize = 0; + for (keys.items) |entry| { + len += std.fmt.count("{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }); + } + break :brk len; + }; + var buf = arena.allocator().alloc(u8, len) catch bun.outOfMemory(); + { + var i: usize = 0; + for (keys.items) |entry| { + const written_slice = std.fmt.bufPrint(buf[i..], "{s}={s}\n", .{ entry.key.slice(), entry.value.slice() }) catch @panic("This should not happen"); + i += written_slice.len; + } + } - /// `--si` - /// Use powers of 1000 not 1024 for sizes - si_units: bool = false, + if (!this.bltn.stdout.needsIO()) { + _ = this.bltn.writeNoIO(.stdout, buf); + this.bltn.done(0); + return Maybe(void).success; + } - /// `-H`, `--dereference-command-line` - /// Follow symbolic links listed on the command line - dereference_cmd_symlinks: bool = false, + this.printing = true; + this.bltn.stdout.enqueue(this, buf); - /// `--dereference-command-line-symlink-to-dir` - /// Follow each command line symbolic link that points to a directory - dereference_cmd_dir_symlinks: bool = false, + return Maybe(void).success; + } - /// `--hide=PATTERN` - /// Do not list entries matching shell PATTERN - hide_pattern: ?[]const u8 = null, + for (args) |arg_raw| { + const arg_sentinel = arg_raw[0..std.mem.len(arg_raw) :0]; + const arg = arg_sentinel[0..arg_sentinel.len]; + if (arg.len == 0) continue; - /// `--hyperlink[=WHEN]` - /// Hyperlink file names; WHEN can be 'always', 'auto', or 'never' - hyperlink: ?[]const u8 = null, + const eqsign_idx = std.mem.indexOfScalar(u8, arg, '=') orelse { + if (!shell.isValidVarName(arg)) { + const buf = this.bltn.fmtErrorArena(.@"export", "`{s}`: not a valid identifier", .{arg}); + return this.writeOutput(.stderr, "{s}\n", .{buf}); + } + this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(arg), EnvStr.initSlice(""), .exported); + continue; + }; - /// `--indicator-style=WORD` - /// Append indicator with style to entry names: 'none', 'slash', 'file-type', 'classify' - indicator_style: ?[]const u8 = null, + const label = arg[0..eqsign_idx]; + const value = arg_sentinel[eqsign_idx + 1 .. :0]; + this.bltn.parentCmd().base.shell.assignVar(this.bltn.parentCmd().base.interpreter, EnvStr.initSlice(label), EnvStr.initSlice(value), .exported); + } - /// `-i`, `--inode` - /// Print the index number of each file - show_inode: bool = false, + this.bltn.done(0); + return Maybe(void).success; + } - /// `-I`, `--ignore=PATTERN` - /// Do not list entries matching shell PATTERN - ignore_pattern: ?[]const u8 = null, + pub fn deinit(this: *Export) void { + log("({s}) deinit", .{@tagName(.@"export")}); + _ = this; + } + }; - /// `-k`, `--kibibytes` - /// Default to 1024-byte blocks for file system usage - kibibytes: bool = false, + pub const Echo = struct { + bltn: *Builtin, - /// `-l` - /// Use a long listing format - long_listing: bool = false, + /// Should be allocated with the arena from Builtin + output: std.ArrayList(u8), - /// `-L`, `--dereference` - /// Show information for the file the symbolic link references - dereference: bool = false, + state: union(enum) { + idle, + waiting, + done, + } = .idle, - /// `-m` - /// Fill width with a comma separated list of entries - comma_separated: bool = false, + pub fn start(this: *Echo) Maybe(void) { + const args = this.bltn.argsSlice(); - /// `-n`, `--numeric-uid-gid` - /// Like -l, but list numeric user and group IDs - numeric_uid_gid: bool = false, + const args_len = args.len; + for (args, 0..) |arg, i| { + const len = std.mem.len(arg); + this.output.appendSlice(arg[0..len]) catch bun.outOfMemory(); + if (i < args_len - 1) { + this.output.append(' ') catch bun.outOfMemory(); + } + } - /// `-N`, `--literal` - /// Print entry names without quoting - literal: bool = false, + this.output.append('\n') catch bun.outOfMemory(); - /// `-o` - /// Like -l, but do not list group information - no_group_info: bool = false, + if (!this.bltn.stdout.needsIO()) { + _ = this.bltn.writeNoIO(.stdout, this.output.items[0..]); + this.state = .done; + this.bltn.done(0); + return Maybe(void).success; + } - /// `-p`, `--indicator-style=slash` - /// Append / indicator to directories - slash_indicator: bool = false, + this.state = .waiting; + this.bltn.stdout.enqueue(this, this.output.items[0..]); + return Maybe(void).success; + } - /// `-q`, `--hide-control-chars` - /// Print ? instead of nongraphic characters - hide_control_chars: bool = false, + pub fn onIOWriterChunk(this: *Echo, e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .waiting); + } - /// `--show-control-chars` - /// Show nongraphic characters as-is - show_control_chars: bool = false, + if (e != null) { + defer e.?.deref(); + this.bltn.done(e.?.getErrno()); + return; + } - /// `-Q`, `--quote-name` - /// Enclose entry names in double quotes - quote_name: bool = false, + this.state = .done; + this.bltn.done(0); + } - /// `--quoting-style=WORD` - /// Use quoting style for entry names - quoting_style: ?[]const u8 = null, + pub fn deinit(this: *Echo) void { + log("({s}) deinit", .{@tagName(.echo)}); + _ = this; + } + }; - /// `-r`, `--reverse` - /// Reverse order while sorting - reverse_order: bool = false, + /// 1 arg => returns absolute path of the arg (not found becomes exit code 1) + /// N args => returns absolute path of each separated by newline, if any path is not found, exit code becomes 1, but continues execution until all args are processed + pub const Which = struct { + bltn: *Builtin, - /// `-R`, `--recursive` - /// List subdirectories recursively - recursive: bool = false, + state: union(enum) { + idle, + one_arg, + multi_args: struct { + args_slice: []const [*:0]const u8, + arg_idx: usize, + had_not_found: bool = false, + state: union(enum) { + none, + waiting_write, + }, + }, + done, + err: JSC.SystemError, + } = .idle, - /// `-s`, `--size` - /// Print the allocated size of each file, in blocks - show_size: bool = false, + pub fn start(this: *Which) Maybe(void) { + const args = this.bltn.argsSlice(); + if (args.len == 0) { + if (!this.bltn.stdout.needsIO()) { + _ = this.bltn.writeNoIO(.stdout, "\n"); + this.bltn.done(1); + return Maybe(void).success; + } + this.state = .one_arg; + this.bltn.stdout.enqueue(this, "\n"); + return Maybe(void).success; + } - /// `-S` - /// Sort by file size, largest first - sort_by_size: bool = false, + if (!this.bltn.stdout.needsIO()) { + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); + var had_not_found = false; + for (args) |arg_raw| { + const arg = arg_raw[0..std.mem.len(arg_raw)]; + const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { + had_not_found = true; + const buf = this.bltn.fmtErrorArena(.which, "{s} not found\n", .{arg}); + _ = this.bltn.writeNoIO(.stdout, buf); + continue; + }; - /// `--sort=WORD` - /// Sort by a specified attribute - sort_method: ?[]const u8 = null, + _ = this.bltn.writeNoIO(.stdout, resolved); + } + this.bltn.done(@intFromBool(had_not_found)); + return Maybe(void).success; + } - /// `--time=WORD` - /// Select which timestamp to use for display or sorting - time_method: ?[]const u8 = null, + this.state = .{ + .multi_args = .{ + .args_slice = args, + .arg_idx = 0, + .state = .none, + }, + }; + this.next(); + return Maybe(void).success; + } - /// `--time-style=TIME_STYLE` - /// Time/date format with -l - time_style: ?[]const u8 = null, + pub fn next(this: *Which) void { + var multiargs = &this.state.multi_args; + if (multiargs.arg_idx >= multiargs.args_slice.len) { + // Done + this.bltn.done(@intFromBool(multiargs.had_not_found)); + return; + } - /// `-t` - /// Sort by time, newest first - sort_by_time: bool = false, + const arg_raw = multiargs.args_slice[multiargs.arg_idx]; + const arg = arg_raw[0..std.mem.len(arg_raw)]; - /// `-T`, `--tabsize=COLS` - /// Assume tab stops at each specified number of columns - tabsize: ?usize = null, + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const PATH = this.bltn.parentCmd().base.shell.export_env.get(EnvStr.initSlice("PATH")) orelse EnvStr.initSlice(""); - /// `-u` - /// Sort by, and show, access time - use_atime: bool = false, + const resolved = which(&path_buf, PATH.slice(), this.bltn.parentCmd().base.shell.cwdZ(), arg) orelse { + multiargs.had_not_found = true; + if (!this.bltn.stdout.needsIO()) { + const buf = this.bltn.fmtErrorArena(null, "{s} not found\n", .{arg}); + _ = this.bltn.writeNoIO(.stdout, buf); + this.argComplete(); + return; + } + multiargs.state = .waiting_write; + this.bltn.stdout.enqueueFmtBltn(this, null, "{s} not found\n", .{arg}); + // yield execution + return; + }; - /// `-U` - /// Do not sort; list entries in directory order - no_sort: bool = false, + if (!this.bltn.stdout.needsIO()) { + const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{resolved}); + _ = this.bltn.writeNoIO(.stdout, buf); + this.argComplete(); + return; + } - /// `-v` - /// Natural sort of (version) numbers within text - natural_sort: bool = false, + multiargs.state = .waiting_write; + this.bltn.stdout.enqueueFmtBltn(this, null, "{s}\n", .{resolved}); + return; + } - /// `-w`, `--width=COLS` - /// Set output width to specified number of columns - output_width: ?usize = null, + fn argComplete(this: *Which) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .multi_args and this.state.multi_args.state == .waiting_write); + } - /// `-x` - /// List entries by lines instead of by columns - list_by_lines: bool = false, + this.state.multi_args.arg_idx += 1; + this.state.multi_args.state = .none; + this.next(); + } - /// `-X` - /// Sort alphabetically by entry extension - sort_by_extension: bool = false, + pub fn onIOWriterChunk(this: *Which, e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .one_arg or + (this.state == .multi_args and this.state.multi_args.state == .waiting_write)); + } - /// `-Z`, `--context` - /// Print any security context of each file - show_context: bool = false, + if (e != null) { + this.state = .{ .err = e.? }; + this.bltn.done(e.?.getErrno()); + return; + } - /// `--zero` - /// End each output line with NUL, not newline - end_with_nul: bool = false, + if (this.state == .one_arg) { + // Calling which with on arguments returns exit code 1 + this.bltn.done(1); + return; + } - /// `-1` - /// List one file per line - one_file_per_line: bool = false, + this.argComplete(); + } - /// `--help` - /// Display help and exit - show_help: bool = false, + pub fn deinit(this: *Which) void { + log("({s}) deinit", .{@tagName(.which)}); + _ = this; + } + }; - /// `--version` - /// Output version information and exit - show_version: bool = false, + /// Some additional behaviour beyond basic `cd `: + /// - `cd` by itself or `cd ~` will always put the user in their home directory. + /// - `cd ~username` will put the user in the home directory of the specified user + /// - `cd -` will put the user in the previous directory + pub const Cd = struct { + bltn: *Builtin, + state: union(enum) { + idle, + waiting_write_stderr, + done, + err: Syscall.Error, + } = .idle, - /// Custom parse error for invalid options - const ParseError = union(enum) { - illegal_option: []const u8, - show_usage, - }; - }; + fn writeStderrNonBlocking(this: *Cd, comptime fmt: []const u8, args: anytype) void { + this.state = .waiting_write_stderr; + this.bltn.stderr.enqueueFmtBltn(this, .cd, fmt, args); + } - pub fn parseOpts(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) { - return this.parseFlags(); + pub fn start(this: *Cd) Maybe(void) { + const args = this.bltn.argsSlice(); + if (args.len > 1) { + this.writeStderrNonBlocking("too many arguments", .{}); + // yield execution + return Maybe(void).success; } - pub fn parseFlags(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) { - const args = this.bltn.argsSlice(); - var idx: usize = 0; - if (args.len == 0) { - return .{ .ok = null }; - } - - while (idx < args.len) : (idx += 1) { - const flag = args[idx]; - switch (this.parseFlag(flag[0..std.mem.len(flag)])) { - .done => { - const filepath_args = args[idx..]; - return .{ .ok = filepath_args }; + const first_arg = args[0][0..std.mem.len(args[0]) :0]; + switch (first_arg[0]) { + '-' => { + switch (this.bltn.parentCmd().base.shell.changePrevCwd(this.bltn.parentCmd().base.interpreter)) { + .result => {}, + .err => |err| { + return this.handleChangeCwdErr(err, this.bltn.parentCmd().base.shell.prevCwdZ()); }, - .continue_parsing => {}, - .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, } - } - - return .{ .err = .show_usage }; + }, + '~' => { + const homedir = this.bltn.parentCmd().base.shell.getHomedir(); + homedir.deref(); + switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, homedir.slice())) { + .result => {}, + .err => |err| return this.handleChangeCwdErr(err, homedir.slice()), + } + }, + else => { + switch (this.bltn.parentCmd().base.shell.changeCwd(this.bltn.parentCmd().base.interpreter, first_arg)) { + .result => {}, + .err => |err| return this.handleChangeCwdErr(err, first_arg), + } + }, } + this.bltn.done(0); + return Maybe(void).success; + } - pub fn parseFlag(this: *Ls, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } { - if (flag.len == 0) return .done; - if (flag[0] != '-') return .done; + fn handleChangeCwdErr(this: *Cd, err: Syscall.Error, new_cwd_: []const u8) Maybe(void) { + const errno: usize = @intCast(err.errno); - // FIXME windows - if (flag.len == 1) return .{ .illegal_option = "-" }; + switch (errno) { + @as(usize, @intFromEnum(bun.C.E.NOTDIR)) => { + if (!this.bltn.stderr.needsIO()) { + const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}", .{new_cwd_}); + _ = this.bltn.writeNoIO(.stderr, buf); + this.state = .done; + this.bltn.done(1); + // yield execution + return Maybe(void).success; + } - const small_flags = flag[1..]; - for (small_flags) |char| { - switch (char) { - 'a' => { - this.opts.show_all = true; - }, - 'A' => { - this.opts.show_almost_all = true; - }, - 'b' => { - this.opts.escape = true; - }, - 'B' => { - this.opts.ignore_backups = true; - }, - 'c' => { - this.opts.use_ctime = true; - }, - 'C' => { - this.opts.list_by_columns = true; - }, - 'd' => { - this.opts.list_directories = true; - }, - 'D' => { - this.opts.dired_mode = true; - }, - 'f' => { - this.opts.unsorted = true; - }, - 'F' => { - this.opts.classify = "always"; - }, - 'g' => { - this.opts.no_owner = true; - }, - 'G' => { - this.opts.no_group = true; - }, - 'h' => { - this.opts.human_readable = true; - }, - 'H' => { - this.opts.dereference_cmd_symlinks = true; - }, - 'i' => { - this.opts.show_inode = true; - }, - 'I' => { - this.opts.ignore_pattern = ""; // This will require additional logic to handle patterns - }, - 'k' => { - this.opts.kibibytes = true; - }, - 'l' => { - this.opts.long_listing = true; - }, - 'L' => { - this.opts.dereference = true; - }, - 'm' => { - this.opts.comma_separated = true; - }, - 'n' => { - this.opts.numeric_uid_gid = true; - }, - 'N' => { - this.opts.literal = true; - }, - 'o' => { - this.opts.no_group_info = true; - }, - 'p' => { - this.opts.slash_indicator = true; - }, - 'q' => { - this.opts.hide_control_chars = true; - }, - 'Q' => { - this.opts.quote_name = true; - }, - 'r' => { - this.opts.reverse_order = true; - }, - 'R' => { - this.opts.recursive = true; - }, - 's' => { - this.opts.show_size = true; - }, - 'S' => { - this.opts.sort_by_size = true; - }, - 't' => { - this.opts.sort_by_time = true; - }, - 'T' => { - this.opts.tabsize = 8; // Default tab size, needs additional handling for custom sizes - }, - 'u' => { - this.opts.use_atime = true; - }, - 'U' => { - this.opts.no_sort = true; - }, - 'v' => { - this.opts.natural_sort = true; - }, - 'w' => { - this.opts.output_width = 0; // Default to no limit, needs additional handling for custom widths - }, - 'x' => { - this.opts.list_by_lines = true; - }, - 'X' => { - this.opts.sort_by_extension = true; - }, - 'Z' => { - this.opts.show_context = true; - }, - '1' => { - this.opts.one_file_per_line = true; - }, - else => { - return .{ .illegal_option = flag[1..2] }; - }, + this.writeStderrNonBlocking("not a directory: {s}", .{new_cwd_}); + return Maybe(void).success; + }, + @as(usize, @intFromEnum(bun.C.E.NOENT)) => { + if (!this.bltn.stderr.needsIO()) { + const buf = this.bltn.fmtErrorArena(.cd, "not a directory: {s}", .{new_cwd_}); + _ = this.bltn.writeNoIO(.stderr, buf); + this.state = .done; + this.bltn.done(1); + // yield execution + return Maybe(void).success; } - } - return .continue_parsing; + this.writeStderrNonBlocking("not a directory: {s}", .{new_cwd_}); + return Maybe(void).success; + }, + else => return Maybe(void).success, } - }; + } - pub const Mv = struct { - bltn: *Builtin, - opts: Opts = .{}, - args: struct { - sources: []const [*:0]const u8 = &[_][*:0]const u8{}, - target: [:0]const u8 = &[0:0]u8{}, - target_fd: ?bun.FileDescriptor = null, - } = .{}, - state: union(enum) { - idle, - check_target: struct { - task: ShellMvCheckTargetTask, - state: union(enum) { - running, - done, - }, - }, - executing: struct { - task_count: usize, - tasks_done: usize = 0, - error_signal: std.atomic.Value(bool), - tasks: []ShellMvBatchedTask, - err: ?Syscall.Error = null, - }, - done, - waiting_write_err: struct { - writer: BufferedWriter, - exit_code: ExitCode, - }, - err: Syscall.Error, - } = .idle, + pub fn onIOWriterChunk(this: *Cd, e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .waiting_write_stderr); + } - pub const ShellMvCheckTargetTask = struct { - const print = bun.Output.scoped(.MvCheckTargetTask, false); - mv: *Mv, + if (e != null) { + defer e.?.deref(); + this.bltn.done(e.?.getErrno()); + return; + } - cwd: bun.FileDescriptor, - target: [:0]const u8, - result: ?Maybe(?bun.FileDescriptor) = null, + this.state = .done; + this.bltn.done(0); + } - task: shell.eval.ShellTask(@This(), EventLoopKind, runFromThreadPool, runFromMainThread, print), + pub fn deinit(this: *Cd) void { + log("({s}) deinit", .{@tagName(.cd)}); + _ = this; + } + }; - pub fn runFromThreadPool(this: *@This()) void { - const fd = switch (Syscall.openat(this.cwd, this.target, os.O.RDONLY | os.O.DIRECTORY, 0)) { - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOTDIR => { - this.result = .{ .result = null }; - }, - else => { - this.result = .{ .err = e }; - }, - } - return; - }, - .result => |fd| fd, - }; - this.result = .{ .result = fd }; - } + pub const Pwd = struct { + bltn: *Builtin, + state: union(enum) { + idle, + waiting_io: struct { + kind: enum { stdout, stderr }, + }, + err, + done, + } = .idle, - pub fn runFromMainThread(this: *@This()) void { - this.mv.checkTargetTaskDone(this); + pub fn start(this: *Pwd) Maybe(void) { + const args = this.bltn.argsSlice(); + if (args.len > 0) { + const msg = "pwd: too many arguments"; + if (this.bltn.stderr.needsIO()) { + this.state = .{ .waiting_io = .{ .kind = .stderr } }; + this.bltn.stderr.enqueue(this, msg); + return Maybe(void).success; } - pub fn runFromMainThreadMini(this: *@This(), _: *void) void { - this.runFromMainThread(); - } - }; + _ = this.bltn.writeNoIO(.stderr, msg); - pub const ShellMvBatchedTask = struct { - const BATCH_SIZE = 5; - const print = bun.Output.scoped(.MvBatchedTask, false); + this.bltn.done(1); + return Maybe(void).success; + } + + const cwd_str = this.bltn.parentCmd().base.shell.cwd(); + if (this.bltn.stdout.needsIO()) { + this.state = .{ .waiting_io = .{ .kind = .stdout } }; + this.bltn.stdout.enqueueFmtBltn(this, null, "{s}\n", .{cwd_str}); + return Maybe(void).success; + } + const buf = this.bltn.fmtErrorArena(null, "{s}\n", .{cwd_str}); - mv: *Mv, - sources: []const [*:0]const u8, - target: [:0]const u8, - target_fd: ?bun.FileDescriptor, - cwd: bun.FileDescriptor, - error_signal: *std.atomic.Value(bool), + _ = this.bltn.writeNoIO(.stdout, buf); - err: ?Syscall.Error = null, + this.state = .done; + this.bltn.done(0); + return Maybe(void).success; + } - task: shell.eval.ShellTask(@This(), EventLoopKind, runFromThreadPool, runFromMainThread, print), + pub fn next(this: *Pwd) void { + while (!(this.state == .err or this.state == .done)) { + switch (this.state) { + .waiting_io => return, + .idle, .done, .err => unreachable, + } + } - pub fn runFromThreadPool(this: *@This()) void { - // Moving multiple entries into a directory - if (this.sources.len > 1) return this.moveMultipleIntoDir(); + if (this.state == .done) { + this.bltn.done(0); + return; + } - const src = this.sources[0][0..std.mem.len(this.sources[0]) :0]; - // Moving entry into directory - if (this.target_fd) |fd| { - _ = fd; + if (this.state == .err) { + this.bltn.done(1); + return; + } + } - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - _ = this.moveInDir(src, &buf); - return; - } + pub fn onIOWriterChunk(this: *Pwd, e: ?JSC.SystemError) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .waiting_io); + } - switch (Syscall.renameat(this.cwd, src, this.cwd, this.target)) { - .err => |e| { - this.err = e; - }, - else => {}, - } - } + if (e != null) { + defer e.?.deref(); + this.state = .err; + this.next(); + return; + } - pub fn moveInDir(this: *@This(), src: [:0]const u8, buf: *[bun.MAX_PATH_BYTES]u8) bool { - var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]); + this.state = .done; - const path_in_dir = std.fs.path.joinZ(fixed_alloc.allocator(), &[_][]const u8{ - "./", - ResolvePath.basename(src), - }) catch { - this.err = Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, .rename); - return false; - }; + this.next(); + } - switch (Syscall.renameat(this.cwd, src, this.target_fd.?, path_in_dir)) { - .err => |e| { - const target_path = ResolvePath.joinZ(&[_][]const u8{ - this.target, - ResolvePath.basename(src), - }, .auto); + pub fn deinit(this: *Pwd) void { + _ = this; + } + }; - this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory()); - return false; - }, - else => {}, - } + pub const Ls = struct { + bltn: *Builtin, + opts: Opts = .{}, - return true; - } + state: union(enum) { + idle, + exec: struct { + err: ?Syscall.Error = null, + task_count: std.atomic.Value(usize), + tasks_done: usize = 0, + output_waiting: usize = 0, + output_done: usize = 0, + }, + waiting_write_err, + done, + } = .idle, - fn moveMultipleIntoDir(this: *@This()) void { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]); + pub fn start(this: *Ls) Maybe(void) { + this.next(); + return Maybe(void).success; + } + + pub fn writeFailingError(this: *Ls, buf: []const u8, exit_code: ExitCode) Maybe(void) { + if (this.bltn.stderr.needsIO()) { + this.bltn.stderr.enqueue(this, buf); + return Maybe(void).success; + } + + _ = this.bltn.writeNoIO(.stderr, buf); + + this.bltn.done(exit_code); + return Maybe(void).success; + } + + fn next(this: *Ls) void { + while (!(this.state == .done)) { + switch (this.state) { + .idle => { + // Will be null if called with no args, in which case we just run once with "." directory + const paths: ?[]const [*:0]const u8 = switch (this.parseOpts()) { + .ok => |paths| paths, + .err => |e| { + const buf = switch (e) { + .illegal_option => |opt_str| this.bltn.fmtErrorArena(.ls, "illegal option -- {s}\n", .{opt_str}), + .show_usage => Builtin.Kind.ls.usageString(), + }; - for (this.sources) |src_raw| { - if (this.error_signal.load(.SeqCst)) return; - defer fixed_alloc.reset(); + _ = this.writeFailingError(buf, 1); + return; + }, + }; + + const task_count = if (paths) |p| p.len else 1; + + this.state = .{ + .exec = .{ + .task_count = std.atomic.Value(usize).init(task_count), + }, + }; - const src = src_raw[0..std.mem.len(src_raw) :0]; - if (!this.moveInDir(src, &buf)) { + const cwd = this.bltn.cwd; + if (paths) |p| { + for (p) |path_raw| { + const path = path_raw[0..std.mem.len(path_raw) :0]; + var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, path, this.bltn.eventLoop()); + task.schedule(); + } + } else { + var task = ShellLsTask.create(this, this.opts, &this.state.exec.task_count, cwd, ".", this.bltn.eventLoop()); + task.schedule(); + } + }, + .exec => { + // It's done + log("Ls(0x{x}, state=exec) Check: tasks_done={d} task_count={d} output_done={d} output_waiting={d}", .{ + @intFromPtr(this), + this.state.exec.tasks_done, + this.state.exec.task_count.load(.Monotonic), + this.state.exec.output_done, + this.state.exec.output_waiting, + }); + if (this.state.exec.tasks_done >= this.state.exec.task_count.load(.Monotonic) and this.state.exec.output_done >= this.state.exec.output_waiting) { + const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0; + this.state = .done; + this.bltn.done(exit_code); return; } - } + return; + }, + .waiting_write_err => { + return; + }, + .done => unreachable, } + } - /// From the man pages of `mv`: - /// ```txt - /// As the rename(2) call does not work across file systems, mv uses cp(1) and rm(1) to accomplish the move. The effect is equivalent to: - /// rm -f destination_path && \ - /// cp -pRP source_file destination && \ - /// rm -rf source_file - /// ``` - fn moveAcrossFilesystems(this: *@This(), src: [:0]const u8, dest: [:0]const u8) void { - _ = this; - _ = src; - _ = dest; - - // TODO - } + this.bltn.done(0); + return; + } - pub fn runFromMainThread(this: *@This()) void { - this.mv.batchedMoveTaskDone(this); - } + pub fn deinit(this: *Ls) void { + _ = this; // autofix + } - pub fn runFromMainThreadMini(this: *@This(), _: *void) void { - this.runFromMainThread(); - } - }; + pub fn onIOWriterChunk(this: *Ls, e: ?JSC.SystemError) void { + if (e) |err| err.deref(); + if (this.state == .waiting_write_err) { + // if (e) |err| return this.bltn.done(1); + return this.bltn.done(1); + } + this.state.exec.output_done += 1; + this.next(); + } + + pub fn onShellLsTaskDone(this: *Ls, task: *ShellLsTask) void { + defer task.deinit(true); + this.state.exec.tasks_done += 1; + var output = task.takeOutput(); + const err_ = task.err; - pub fn start(this: *Mv) Maybe(void) { - return this.next(); + // TODO: Reuse the *ShellLsTask allocation + const output_task: *ShellLsOutputTask = bun.new(ShellLsOutputTask, .{ + .parent = this, + .output = .{ .arrlist = output.moveToUnmanaged() }, + .state = .waiting_write_err, + }); + + if (err_) |err| { + this.state.exec.err = err; + const error_string = this.bltn.taskErrorToString(.ls, err); + output_task.start(error_string); + return; } + output_task.start(null); + } - pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Maybe(void) { - if (this.bltn.stderr.needsIO()) { - this.state = .{ - .waiting_write_err = .{ - .writer = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - .exit_code = exit_code, - }, - }; - this.state.waiting_write_err.writer.writeIfPossible(false); - return Maybe(void).success; - } + pub const ShellLsOutputTask = OutputTask(Ls, .{ + .writeErr = ShellLsOutputTaskVTable.writeErr, + .onWriteErr = ShellLsOutputTaskVTable.onWriteErr, + .writeOut = ShellLsOutputTaskVTable.writeOut, + .onWriteOut = ShellLsOutputTaskVTable.onWriteOut, + .onDone = ShellLsOutputTaskVTable.onDone, + }); - if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| { - return .{ .err = e }; + const ShellLsOutputTaskVTable = struct { + pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) CoroutineResult { + if (this.bltn.stderr.needsIO()) { + this.state.exec.output_waiting += 1; + this.bltn.stderr.enqueue(childptr, errbuf); + return .yield; } + _ = this.bltn.writeNoIO(.stderr, errbuf); + return .cont; + } - this.bltn.done(exit_code); - return Maybe(void).success; + pub fn onWriteErr(this: *Ls) void { + this.state.exec.output_done += 1; } - pub fn next(this: *Mv) Maybe(void) { - while (!(this.state == .done or this.state == .err)) { - switch (this.state) { - .idle => { - if (this.parseOpts().asErr()) |e| { - const buf = switch (e) { - .illegal_option => |opt_str| this.bltn.fmtErrorArena(.mv, "illegal option -- {s}\n", .{opt_str}), - .show_usage => Builtin.Kind.mv.usageString(), - }; + pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) CoroutineResult { + if (this.bltn.stdout.needsIO()) { + this.state.exec.output_waiting += 1; + this.bltn.stdout.enqueue(childptr, output.slice()); + return .yield; + } + _ = this.bltn.writeNoIO(.stdout, output.slice()); + return .cont; + } - return this.writeFailingError(buf, 1); - } - this.state = .{ - .check_target = .{ - .task = ShellMvCheckTargetTask{ - .mv = this, - .cwd = this.bltn.parentCmd().base.shell.cwd_fd, - .target = this.args.target, - .task = .{ - // .event_loop = JSC.VirtualMachine.get().eventLoop(), - .event_loop = event_loop_ref.get(), - }, - }, - .state = .running, - }, - }; - this.state.check_target.task.task.schedule(); - return Maybe(void).success; - }, - .check_target => { - if (this.state.check_target.state == .running) return Maybe(void).success; - const check_target = &this.state.check_target; + pub fn onWriteOut(this: *Ls) void { + this.state.exec.output_done += 1; + } - if (comptime bun.Environment.allow_assert) { - std.debug.assert(check_target.task.result != null); - } + pub fn onDone(this: *Ls) void { + this.next(); + } + }; - const maybe_fd: ?bun.FileDescriptor = switch (check_target.task.result.?) { - .err => |e| brk: { - defer bun.default_allocator.free(e.path); - switch (e.getErrno()) { - bun.C.E.NOENT => { - // Means we are renaming entry, not moving to a directory - if (this.args.sources.len == 1) break :brk null; - - const buf = this.bltn.fmtErrorArena(.mv, "{s}: No such file or directory\n", .{this.args.target}); - return this.writeFailingError(buf, 1); - }, - else => { - const sys_err = e.toSystemError(); - const buf = this.bltn.fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() }); - return this.writeFailingError(buf, 1); - }, - } - }, - .result => |maybe_fd| maybe_fd, - }; + pub const ShellLsTask = struct { + const print = bun.Output.scoped(.ShellLsTask, false); + ls: *Ls, + opts: Opts, - // Trying to move multiple files into a file - if (maybe_fd == null and this.args.sources.len > 1) { - const buf = this.bltn.fmtErrorArena(.mv, "{s} is not a directory\n", .{this.args.target}); - return this.writeFailingError(buf, 1); - } + is_root: bool = true, + task_count: *std.atomic.Value(usize), - const task_count = brk: { - const sources_len: f64 = @floatFromInt(this.args.sources.len); - const batch_size: f64 = @floatFromInt(ShellMvBatchedTask.BATCH_SIZE); - const task_count: usize = @intFromFloat(@ceil(sources_len / batch_size)); - break :brk task_count; - }; + cwd: bun.FileDescriptor, + /// Should be allocated with bun.default_allocator + path: [:0]const u8 = &[0:0]u8{}, + /// Should use bun.default_allocator + output: std.ArrayList(u8), + is_absolute: bool = false, + err: ?Syscall.Error = null, + result_kind: enum { file, dir, idk } = .idk, + + event_loop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, + task: JSC.WorkPoolTask = .{ + .callback = workPoolCallback, + }, - this.args.target_fd = maybe_fd; - const cwd_fd = this.bltn.parentCmd().base.shell.cwd_fd; - const tasks = this.bltn.arena.allocator().alloc(ShellMvBatchedTask, task_count) catch bun.outOfMemory(); - // Initialize tasks - { - var count = task_count; - const count_per_task = this.args.sources.len / ShellMvBatchedTask.BATCH_SIZE; - var i: usize = 0; - var j: usize = 0; - while (i < tasks.len -| 1) : (i += 1) { - j += count_per_task; - const sources = this.args.sources[j .. j + count_per_task]; - count -|= count_per_task; - tasks[i] = ShellMvBatchedTask{ - .mv = this, - .cwd = cwd_fd, - .target = this.args.target, - .target_fd = this.args.target_fd, - .sources = sources, - // We set this later - .error_signal = undefined, - .task = .{ - .event_loop = event_loop_ref.get(), - }, - }; - } + pub fn schedule(this: *@This()) void { + JSC.WorkPool.schedule(&this.task); + } - // Give remainder to last task - if (count > 0) { - const sources = this.args.sources[j .. j + count]; - tasks[i] = ShellMvBatchedTask{ - .mv = this, - .cwd = cwd_fd, - .target = this.args.target, - .target_fd = this.args.target_fd, - .sources = sources, - // We set this later - .error_signal = undefined, - .task = .{ - .event_loop = event_loop_ref.get(), - }, - }; - } - } + pub fn create(ls: *Ls, opts: Opts, task_count: *std.atomic.Value(usize), cwd: bun.FileDescriptor, path: [:0]const u8, event_loop: JSC.EventLoopHandle) *@This() { + const task = bun.default_allocator.create(@This()) catch bun.outOfMemory(); + task.* = @This(){ + .ls = ls, + .opts = opts, + .cwd = cwd, + .path = bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory(), + .output = std.ArrayList(u8).init(bun.default_allocator), + // .event_loop = event_loop orelse JSC.VirtualMachine.get().eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(event_loop), + .event_loop = event_loop, + .task_count = task_count, + }; + return task; + } - this.state = .{ - .executing = .{ - .task_count = task_count, - .error_signal = std.atomic.Value(bool).init(false), - .tasks = tasks, - }, - }; + pub fn enqueue(this: *@This(), path: [:0]const u8) void { + print("enqueue: {s}", .{path}); + const new_path = this.join( + bun.default_allocator, + &[_][]const u8{ + this.path[0..this.path.len], + path[0..path.len], + }, + this.is_absolute, + ); - for (this.state.executing.tasks) |*t| { - t.error_signal = &this.state.executing.error_signal; - t.task.schedule(); - } + var subtask = @This().create(this.ls, this.opts, this.task_count, this.cwd, new_path, this.event_loop); + _ = this.task_count.fetchAdd(1, .Monotonic); + subtask.is_root = false; + subtask.schedule(); + } - return Maybe(void).success; - }, - .executing => { - const exec = &this.state.executing; - _ = exec; - // if (exec.state == .idle) { - // // 1. Check if target is directory or file - // } - }, - .waiting_write_err => { - return Maybe(void).success; - }, - .done, .err => unreachable, - } + inline fn join(this: *@This(), alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 { + _ = this; // autofix + if (!is_absolute) { + // If relative paths enabled, stdlib join is preferred over + // ResolvePath.joinBuf because it doesn't try to normalize the path + return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory(); } - if (this.state == .done) { - this.bltn.done(0); - return Maybe(void).success; - } + const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory(); - this.bltn.done(this.state.err.errno); - return Maybe(void).success; + return out; } - pub fn onBufferedWriterDone(this: *Mv, e: ?Syscall.Error) void { - switch (this.state) { - .waiting_write_err => { - if (e != null) { - this.state.err = e.?; - _ = this.next(); - return; + pub fn run(this: *@This()) void { + const fd = switch (ShellSyscall.openat(this.cwd, this.path, os.O.RDONLY | os.O.DIRECTORY, 0)) { + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOENT => { + this.err = this.errorWithPath(e, this.path); + }, + bun.C.E.NOTDIR => { + this.result_kind = .file; + this.addEntry(this.path); + }, + else => { + this.err = this.errorWithPath(e, this.path); + }, } - this.bltn.done(this.state.waiting_write_err.exit_code); return; }, - else => |t| std.debug.panic("Unexpected state .{s} in Bun shell 'mv' builtin.", .{@tagName(t)}), - } - } - - pub fn checkTargetTaskDone(this: *Mv, task: *ShellMvCheckTargetTask) void { - _ = task; - - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .check_target); - std.debug.assert(this.state.check_target.task.result != null); - } - - this.state.check_target.state = .done; - _ = this.next(); - return; - } + .result => |fd| fd, + }; - pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert(this.state == .executing); - std.debug.assert(this.state.executing.tasks_done < this.state.executing.task_count); + defer { + _ = Syscall.close(fd); + print("run done", .{}); } - var exec = &this.state.executing; - - if (task.err) |err| { - exec.error_signal.store(true, .SeqCst); - if (exec.err == null) { - exec.err = err; - } else { - bun.default_allocator.free(err.path); + if (!this.opts.list_directories) { + if (!this.is_root) { + const writer = this.output.writer(); + std.fmt.format(writer, "{s}:\n", .{this.path}) catch bun.outOfMemory(); } - } - exec.tasks_done += 1; - if (exec.tasks_done >= exec.task_count) { - if (exec.err) |err| { - const buf = this.bltn.fmtErrorArena(.ls, "{s}\n", .{err.toSystemError().message.byteSlice()}); - _ = this.writeFailingError(buf, err.errno); - return; + var iterator = DirIterator.iterate(fd.asDir(), .u8); + var entry = iterator.next(); + + while (switch (entry) { + .err => |e| { + this.err = this.errorWithPath(e, this.path); + return; + }, + .result => |ent| ent, + }) |current| : (entry = iterator.next()) { + this.addEntry(current.name.sliceAssumeZ()); + if (current.kind == .directory and this.opts.recursive) { + this.enqueue(current.name.sliceAssumeZ()); + } } - this.state = .done; - _ = this.next(); return; } + + const writer = this.output.writer(); + std.fmt.format(writer, "{s}\n", .{this.path}) catch bun.outOfMemory(); + return; } - pub fn deinit(this: *Mv) void { - if (this.args.target_fd != null and this.args.target_fd.? != bun.invalid_fd) { - _ = Syscall.close(this.args.target_fd.?); + fn shouldSkipEntry(this: *@This(), name: [:0]const u8) bool { + if (this.opts.show_all) return false; + if (this.opts.show_almost_all) { + if (bun.strings.eqlComptime(name[0..1], ".") or bun.strings.eqlComptime(name[0..2], "..")) return true; } + return false; } - const Opts = struct { - /// `-f` - /// - /// Do not prompt for confirmation before overwriting the destination path. (The -f option overrides any previous -i or -n options.) - force_overwrite: bool = true, - /// `-h` - /// - /// If the target operand is a symbolic link to a directory, do not follow it. This causes the mv utility to rename the file source to the destination path target rather than moving source into the - /// directory referenced by target. - no_dereference: bool = false, - /// `-i` - /// - /// Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file. If the response from the standard input begins with the character ‘y’ or ‘Y’, the move is - /// attempted. (The -i option overrides any previous -f or -n options.) - interactive_mode: bool = false, - /// `-n` - /// - /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.) - no_overwrite: bool = false, - /// `-v` - /// - /// Cause mv to be verbose, showing files after they are moved. - verbose_output: bool = false, - - const ParseError = union(enum) { - illegal_option: []const u8, - show_usage, - }; - }; - - pub fn parseOpts(this: *Mv) Result(void, Opts.ParseError) { - const filepath_args = switch (this.parseFlags()) { - .ok => |args| args, - .err => |e| return .{ .err = e }, - }; - - if (filepath_args.len < 2) { - return .{ .err = .show_usage }; - } + // TODO more complex output like multi-column + fn addEntry(this: *@This(), name: [:0]const u8) void { + const skip = this.shouldSkipEntry(name); + print("Entry: (skip={}) {s} :: {s}", .{ skip, this.path, name }); + if (skip) return; + this.output.ensureUnusedCapacity(name.len + 1) catch bun.outOfMemory(); + this.output.appendSlice(name) catch bun.outOfMemory(); + // FIXME TODO non ascii/utf-8 + this.output.append('\n') catch bun.outOfMemory(); + } - this.args.sources = filepath_args[0 .. filepath_args.len - 1]; - this.args.target = filepath_args[filepath_args.len - 1][0..std.mem.len(filepath_args[filepath_args.len - 1]) :0]; + fn errorWithPath(this: *@This(), err: Syscall.Error, path: [:0]const u8) Syscall.Error { + _ = this; + return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory()); + } - return .ok; + pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { + var this: *@This() = @fieldParentPtr(@This(), "task", task); + this.run(); + this.doneLogic(); } - pub fn parseFlags(this: *Mv) Result([]const [*:0]const u8, Opts.ParseError) { - const args = this.bltn.argsSlice(); - var idx: usize = 0; - if (args.len == 0) { - return .{ .err = .show_usage }; + fn doneLogic(this: *@This()) void { + print("Done", .{}); + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); } - while (idx < args.len) : (idx += 1) { - const flag = args[idx]; - switch (this.parseFlag(flag[0..std.mem.len(flag)])) { - .done => { - const filepath_args = args[idx..]; - return .{ .ok = filepath_args }; - }, - .continue_parsing => {}, - .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, - } - } + // if (this.parent) |parent| { + // _ = parent.children_done.fetchAdd(1, .Monotonic); + // if (parent.childrenAreDone()) parent.doneLogic(); + // } + } - return .{ .err = .show_usage }; + pub fn takeOutput(this: *@This()) std.ArrayList(u8) { + const ret = this.output; + this.output = std.ArrayList(u8).init(bun.default_allocator); + return ret; } - pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } { - if (flag.len == 0) return .done; - if (flag[0] != '-') return .done; + pub fn runFromMainThread(this: *@This()) void { + print("runFromMainThread", .{}); + this.ls.onShellLsTaskDone(this); + } - const small_flags = flag[1..]; - for (small_flags) |char| { - switch (char) { - 'f' => { - this.opts.force_overwrite = true; - this.opts.interactive_mode = false; - this.opts.no_overwrite = false; - }, - 'h' => { - this.opts.no_dereference = true; - }, - 'i' => { - this.opts.interactive_mode = true; - this.opts.force_overwrite = false; - this.opts.no_overwrite = false; - }, - 'n' => { - this.opts.no_overwrite = true; - this.opts.force_overwrite = false; - this.opts.interactive_mode = false; - }, - 'v' => { - this.opts.verbose_output = true; - }, - else => { - return .{ .illegal_option = "-" }; - }, - } - } + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } - return .continue_parsing; + pub fn deinit(this: *@This(), comptime free_this: bool) void { + print("deinit {s}", .{if (free_this) "free_this=true" else "free_this=false"}); + bun.default_allocator.free(this.path); + this.output.deinit(); + if (comptime free_this) bun.default_allocator.destroy(this); } }; - pub const Rm = struct { - bltn: *Builtin, - opts: Opts, - state: union(enum) { - idle, - parse_opts: struct { - args_slice: []const [*:0]const u8, - idx: u32 = 0, - state: union(enum) { - normal, - wait_write_err: BufferedWriter, - } = .normal, - }, - exec: struct { - // task: RmTask, - filepath_args: []const [*:0]const u8, - total_tasks: usize, - err: ?Syscall.Error = null, - lock: std.Thread.Mutex = std.Thread.Mutex{}, - error_signal: std.atomic.Value(bool) = .{ .raw = false }, - output_queue: std.DoublyLinkedList(BlockingOutput) = .{}, - output_done: std.atomic.Value(usize) = .{ .raw = 0 }, - output_count: std.atomic.Value(usize) = .{ .raw = 0 }, - state: union(enum) { - idle, - waiting: struct { - tasks_done: usize = 0, - }, + const Opts = struct { + /// `-a`, `--all` + /// Do not ignore entries starting with . + show_all: bool = false, - pub fn tasksDone(this: *@This()) usize { - return switch (this.*) { - .idle => 0, - .waiting => this.waiting.tasks_done, - }; - } - }, + /// `-A`, `--almost-all` + /// Do not list implied . and .. + show_almost_all: bool = true, - fn incrementOutputCount(this: *@This(), comptime thevar: @Type(.EnumLiteral)) void { - @fence(.SeqCst); - var atomicvar = &@field(this, @tagName(thevar)); - const result = atomicvar.fetchAdd(1, .SeqCst); - log("[rm] {s}: {d} + 1", .{ @tagName(thevar), result }); - return; - } + /// `--author` + /// With -l, print the author of each file + show_author: bool = false, - fn getOutputCount(this: *@This(), comptime thevar: @Type(.EnumLiteral)) usize { - @fence(.SeqCst); - var atomicvar = &@field(this, @tagName(thevar)); - return atomicvar.load(.SeqCst); - } - }, - done: struct { exit_code: ExitCode }, - err: Syscall.Error, - } = .idle, + /// `-b`, `--escape` + /// Print C-style escapes for nongraphic characters + escape: bool = false, - pub const Opts = struct { - /// `--no-preserve-root` / `--preserve-root` - /// - /// If set to false, then allow the recursive removal of the root directory. - /// Safety feature to prevent accidental deletion of the root directory. - preserve_root: bool = true, + /// `--block-size=SIZE` + /// With -l, scale sizes by SIZE when printing them; e.g., '--block-size=M' + block_size: ?usize = null, - /// `-f`, `--force` - /// - /// Ignore nonexistent files and arguments, never prompt. - force: bool = false, + /// `-B`, `--ignore-backups` + /// Do not list implied entries ending with ~ + ignore_backups: bool = false, - /// Configures how the user should be prompted on removal of files. - prompt_behaviour: PromptBehaviour = .never, + /// `-c` + /// Sort by, and show, ctime (time of last change of file status information); affects sorting and display based on options + use_ctime: bool = false, - /// `-r`, `-R`, `--recursive` - /// - /// Remove directories and their contents recursively. - recursive: bool = false, + /// `-C` + /// List entries by columns + list_by_columns: bool = false, - /// `-v`, `--verbose` - /// - /// Explain what is being done (prints which files/dirs are being deleted). - verbose: bool = false, + /// `--color[=WHEN]` + /// Color the output; WHEN can be 'always', 'auto', or 'never' + color: ?[]const u8 = null, - /// `-d`, `--dir` - /// - /// Remove empty directories. This option permits you to remove a directory - /// without specifying `-r`/`-R`/`--recursive`, provided that the directory is - /// empty. - remove_empty_dirs: bool = false, - - const PromptBehaviour = union(enum) { - /// `--interactive=never` - /// - /// Default - never, - - /// `-I`, `--interactive=once` - /// - /// Once before removing more than three files, or when removing recursively. - once: struct { - removed_count: u32 = 0, - }, + /// `-d`, `--directory` + /// List directories themselves, not their contents + list_directories: bool = false, - /// `-i`, `--interactive=always` - /// - /// Prompt before every removal. - always, - }; - }; + /// `-D`, `--dired` + /// Generate output designed for Emacs' dired mode + dired_mode: bool = false, - pub fn start(this: *Rm) Maybe(void) { - return this.next(); - } + /// `-f` + /// List all entries in directory order + unsorted: bool = false, - pub noinline fn next(this: *Rm) Maybe(void) { - while (this.state != .done and this.state != .err) { - switch (this.state) { - .idle => { - this.state = .{ - .parse_opts = .{ - .args_slice = this.bltn.argsSlice(), - }, - }; - continue; - }, - .parse_opts => { - var parse_opts = &this.state.parse_opts; - switch (parse_opts.state) { - .normal => { - // This means there were no arguments or only - // flag arguments meaning no positionals, in - // either case we must print the usage error - // string - if (parse_opts.idx >= parse_opts.args_slice.len) { - const error_string = Builtin.Kind.usageString(.rm); - if (this.bltn.stderr.needsIO()) { - parse_opts.state = .{ - .wait_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - parse_opts.state.wait_write_err.writeIfPossible(false); - return Maybe(void).success; - } + /// `-F`, `--classify[=WHEN]` + /// Append indicator (one of */=>@|) to entries; WHEN can be 'always', 'auto', or 'never' + classify: ?[]const u8 = null, - switch (this.bltn.writeNoIO(.stderr, error_string)) { - .result => {}, - .err => |e| return Maybe(void).initErr(e), - } - this.bltn.done(1); - return Maybe(void).success; - } + /// `--file-type` + /// Likewise, except do not append '*' + file_type: bool = false, - const idx = parse_opts.idx; + /// `--format=WORD` + /// Specify format: 'across', 'commas', 'horizontal', 'long', 'single-column', 'verbose', 'vertical' + format: ?[]const u8 = null, - const arg_raw = parse_opts.args_slice[idx]; - const arg = arg_raw[0..std.mem.len(arg_raw)]; + /// `--full-time` + /// Like -l --time-style=full-iso + full_time: bool = false, - switch (parseFlag(&this.opts, this.bltn, arg)) { - .continue_parsing => { - parse_opts.idx += 1; - continue; - }, - .done => { - if (this.opts.recursive) { - this.opts.remove_empty_dirs = true; - } + /// `-g` + /// Like -l, but do not list owner + no_owner: bool = false, - if (this.opts.prompt_behaviour != .never) { - const buf = "rm: \"-i\" is not supported yet"; - if (this.bltn.stderr.needsIO()) { - parse_opts.state = .{ - .wait_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = buf, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - parse_opts.state.wait_write_err.writeIfPossible(false); - continue; - } + /// `--group-directories-first` + /// Group directories before files + group_directories_first: bool = false, - if (this.bltn.writeNoIO(.stderr, buf).asErr()) |e| - return Maybe(void).initErr(e); + /// `-G`, `--no-group` + /// In a long listing, don't print group names + no_group: bool = false, - this.bltn.done(1); - return Maybe(void).success; - } + /// `-h`, `--human-readable` + /// With -l and -s, print sizes like 1K 234M 2G etc. + human_readable: bool = false, - const filepath_args_start = idx; - const filepath_args = parse_opts.args_slice[filepath_args_start..]; - - // Check that non of the paths will delete the root - { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - const cwd = switch (Syscall.getcwd(&buf)) { - .err => |err| { - return .{ .err = err }; - }, - .result => |cwd| cwd, - }; + /// `--si` + /// Use powers of 1000 not 1024 for sizes + si_units: bool = false, - for (filepath_args) |filepath| { - const path = filepath[0..bun.len(filepath)]; - const resolved_path = if (ResolvePath.Platform.auto.isAbsolute(path)) path else bun.path.join(&[_][]const u8{ cwd, path }, .auto); - const is_root = brk: { - const normalized = bun.path.normalizeString(resolved_path, false, .auto); - const dirname = ResolvePath.dirname(normalized, .auto); - const is_root = std.mem.eql(u8, dirname, ""); - break :brk is_root; - }; + /// `-H`, `--dereference-command-line` + /// Follow symbolic links listed on the command line + dereference_cmd_symlinks: bool = false, - if (is_root) { - const error_string = this.bltn.fmtErrorArena(.rm, "\"{s}\" may not be removed\n", .{resolved_path}); - if (this.bltn.stderr.needsIO()) { - parse_opts.state = .{ - .wait_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - parse_opts.state.wait_write_err.writeIfPossible(false); - return Maybe(void).success; - } - - switch (this.bltn.writeNoIO(.stderr, error_string)) { - .result => {}, - .err => |e| return Maybe(void).initErr(e), - } - this.bltn.done(1); - return Maybe(void).success; - } - } - } + /// `--dereference-command-line-symlink-to-dir` + /// Follow each command line symbolic link that points to a directory + dereference_cmd_dir_symlinks: bool = false, - const total_tasks = filepath_args.len; - this.state = .{ - .exec = .{ - .filepath_args = filepath_args, - .total_tasks = total_tasks, - .state = .idle, - .output_done = std.atomic.Value(usize).init(0), - .output_count = std.atomic.Value(usize).init(0), - }, - }; - // this.state.exec.task.schedule(); - // return Maybe(void).success; - continue; - }, - .illegal_option => { - const error_string = "rm: illegal option -- -\n"; - if (this.bltn.stderr.needsIO()) { - parse_opts.state = .{ - .wait_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - parse_opts.state.wait_write_err.writeIfPossible(false); - return Maybe(void).success; - } + /// `--hide=PATTERN` + /// Do not list entries matching shell PATTERN + hide_pattern: ?[]const u8 = null, - switch (this.bltn.writeNoIO(.stderr, error_string)) { - .result => {}, - .err => |e| return Maybe(void).initErr(e), - } - this.bltn.done(1); - return Maybe(void).success; - }, - .illegal_option_with_flag => { - const flag = arg; - const error_string = this.bltn.fmtErrorArena(.rm, "illegal option -- {s}\n", .{flag[1..]}); - if (this.bltn.stderr.needsIO()) { - parse_opts.state = .{ - .wait_write_err = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - }; - parse_opts.state.wait_write_err.writeIfPossible(false); - return Maybe(void).success; - } + /// `--hyperlink[=WHEN]` + /// Hyperlink file names; WHEN can be 'always', 'auto', or 'never' + hyperlink: ?[]const u8 = null, - switch (this.bltn.writeNoIO(.stderr, error_string)) { - .result => {}, - .err => |e| return Maybe(void).initErr(e), - } - this.bltn.done(1); - return Maybe(void).success; - }, - } - }, - .wait_write_err => { - // Errored - if (parse_opts.state.wait_write_err.err) |e| { - this.state = .{ .err = e }; - continue; - } + /// `--indicator-style=WORD` + /// Append indicator with style to entry names: 'none', 'slash', 'file-type', 'classify' + indicator_style: ?[]const u8 = null, - // Done writing - if (this.state.parse_opts.state.wait_write_err.remain.len == 0) { - this.state = .{ .done = .{ .exit_code = 0 } }; - continue; - } + /// `-i`, `--inode` + /// Print the index number of each file + show_inode: bool = false, - // yield execution to continue writing - return Maybe(void).success; - }, - } - }, - .exec => { - const cwd = this.bltn.parentCmd().base.shell.cwd_fd; - // Schedule task - if (this.state.exec.state == .idle) { - this.state.exec.state = .{ .waiting = .{} }; - for (this.state.exec.filepath_args) |root_raw| { - const root = root_raw[0..std.mem.len(root_raw)]; - const root_path_string = bun.PathString.init(root[0..root.len]); - const is_absolute = ResolvePath.Platform.auto.isAbsolute(root); - var task = ShellRmTask.create(root_path_string, this, cwd, &this.state.exec.error_signal, is_absolute); - task.schedule(); - // task. - } - } + /// `-I`, `--ignore=PATTERN` + /// Do not list entries matching shell PATTERN + ignore_pattern: ?[]const u8 = null, - // do nothing - return Maybe(void).success; - }, - .done, .err => unreachable, - } - } + /// `-k`, `--kibibytes` + /// Default to 1024-byte blocks for file system usage + kibibytes: bool = false, - if (this.state == .done) { - this.bltn.done(0); - return Maybe(void).success; - } + /// `-l` + /// Use a long listing format + long_listing: bool = false, - if (this.state == .err) { - this.bltn.done(this.state.err.errno); - return Maybe(void).success; - } + /// `-L`, `--dereference` + /// Show information for the file the symbolic link references + dereference: bool = false, - return Maybe(void).success; - } + /// `-m` + /// Fill width with a comma separated list of entries + comma_separated: bool = false, - pub fn onBufferedWriterDone(this: *Rm, e: ?Syscall.Error) void { - if (comptime bun.Environment.allow_assert) { - std.debug.assert((this.state == .parse_opts and this.state.parse_opts.state == .wait_write_err) or - (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_queue.len > 0)); - } + /// `-n`, `--numeric-uid-gid` + /// Like -l, but list numeric user and group IDs + numeric_uid_gid: bool = false, - if (this.state == .exec and this.state.exec.state == .waiting) { - log("[rm] output done={d} output count={d}", .{ this.state.exec.getOutputCount(.output_done), this.state.exec.getOutputCount(.output_count) }); - this.state.exec.incrementOutputCount(.output_done); - // _ = this.state.exec.output_done.fetchAdd(1, .Monotonic); - var queue = &this.state.exec.output_queue; - var first = queue.popFirst().?; - defer { - first.data.deinit(); - bun.default_allocator.destroy(first); - } - if (first.next) |next_writer| { - next_writer.data.writer.writeIfPossible(false); - } else { - if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { - this.bltn.done(if (this.state.exec.err != null) 1 else 0); - return; - } - } - return; - } + /// `-N`, `--literal` + /// Print entry names without quoting + literal: bool = false, - if (e != null) { - this.state = .{ .err = e.? }; - this.bltn.done(e.?.errno); - return; - } + /// `-o` + /// Like -l, but do not list group information + no_group_info: bool = false, - this.bltn.done(1); - return; - } + /// `-p`, `--indicator-style=slash` + /// Append / indicator to directories + slash_indicator: bool = false, - pub fn writeToStdoutFromAsyncTask(this: *Rm, comptime fmt: []const u8, args: anytype) Maybe(void) { - const buf = this.rm.bltn.fmtErrorArena(null, fmt, args); - if (!this.rm.bltn.stdout.needsIO()) { - this.state.exec.lock.lock(); - defer this.state.exec.lock.unlock(); - return switch (this.rm.bltn.writeNoIO(.stdout, buf)) { - .result => Maybe(void).success, - .err => |e| Maybe(void).initErr(e), - }; - } + /// `-q`, `--hide-control-chars` + /// Print ? instead of nongraphic characters + hide_control_chars: bool = false, - var written: usize = 0; - while (written < buf.len) : (written += switch (Syscall.write(this.rm.bltn.stdout.fd, buf)) { - .err => |e| return Maybe(void).initErr(e), - .result => |n| n, - }) {} + /// `--show-control-chars` + /// Show nongraphic characters as-is + show_control_chars: bool = false, - return Maybe(void).success; - } + /// `-Q`, `--quote-name` + /// Enclose entry names in double quotes + quote_name: bool = false, - pub fn deinit(this: *Rm) void { - _ = this; - } + /// `--quoting-style=WORD` + /// Use quoting style for entry names + quoting_style: ?[]const u8 = null, - const ParseFlagsResult = enum { - continue_parsing, - done, - illegal_option, - illegal_option_with_flag, - }; + /// `-r`, `--reverse` + /// Reverse order while sorting + reverse_order: bool = false, - fn parseFlag(this: *Opts, bltn: *Builtin, flag: []const u8) ParseFlagsResult { - _ = bltn; - if (flag.len == 0) return .done; - if (flag[0] != '-') return .done; - if (flag.len > 2 and flag[1] == '-') { - if (bun.strings.eqlComptime(flag, "--preserve-root")) { - this.preserve_root = true; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--no-preserve-root")) { - this.preserve_root = false; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--recursive")) { - this.recursive = true; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--verbose")) { - this.verbose = true; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--dir")) { - this.remove_empty_dirs = true; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--interactive=never")) { - this.prompt_behaviour = .never; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--interactive=once")) { - this.prompt_behaviour = .{ .once = .{} }; - return .continue_parsing; - } else if (bun.strings.eqlComptime(flag, "--interactive=always")) { - this.prompt_behaviour = .always; - return .continue_parsing; - } + /// `-R`, `--recursive` + /// List subdirectories recursively + recursive: bool = false, - // try bltn.write_err(&bltn.stderr, .rm, "illegal option -- -\n", .{}); - return .illegal_option; - } + /// `-s`, `--size` + /// Print the allocated size of each file, in blocks + show_size: bool = false, - const small_flags = flag[1..]; - for (small_flags) |char| { - switch (char) { - 'f' => { - this.force = true; - this.prompt_behaviour = .never; - }, - 'r', 'R' => { - this.recursive = true; - }, - 'v' => { - this.verbose = true; - }, - 'd' => { - this.remove_empty_dirs = true; - }, - 'i' => { - this.prompt_behaviour = .{ .once = .{} }; - }, - 'I' => { - this.prompt_behaviour = .always; - }, - else => { - // try bltn.write_err(&bltn.stderr, .rm, "illegal option -- {s}\n", .{flag[1..]}); - return .illegal_option_with_flag; - }, - } - } + /// `-S` + /// Sort by file size, largest first + sort_by_size: bool = false, - return .continue_parsing; - } - - pub fn onAsyncTaskDone(this: *Rm, task: *ShellRmTask) void { - var exec = &this.state.exec; - const tasks_done = switch (exec.state) { - .idle => @panic("Unexpected state .idle in Bun shell 'rm' builtin."), - .waiting => brk: { - exec.state.waiting.tasks_done += 1; - const amt = exec.state.waiting.tasks_done; - if (task.err) |err| { - exec.err = err; - const error_string = this.bltn.taskErrorToString(.rm, err); - if (!this.bltn.stderr.needsIO()) { - if (this.bltn.writeNoIO(.stderr, error_string).asErr()) |e| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(e)); - return; - } - } else { - const bo = BlockingOutput{ - .writer = BufferedWriter{ - .fd = this.bltn.stderr.expectFd(), - .remain = error_string, - .parent = BufferedWriter.ParentPtr.init(this), - .bytelist = this.bltn.stdBufferedBytelist(.stderr), - }, - .arr = std.ArrayList(u8).init(bun.default_allocator), - }; - exec.incrementOutputCount(.output_count); - // _ = exec.output_count.fetchAdd(1, .Monotonic); - return this.queueBlockingOutput(bo); - } - } - break :brk amt; - }, - }; + /// `--sort=WORD` + /// Sort by a specified attribute + sort_method: ?[]const u8 = null, - // Wait until all tasks done and all output is written - if (tasks_done >= this.state.exec.total_tasks and - exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count)) - { - this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } }; - _ = this.next(); - return; - } - } + /// `--time=WORD` + /// Select which timestamp to use for display or sorting + time_method: ?[]const u8 = null, - fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) void { - if (!this.bltn.stdout.needsIO()) { - if (this.bltn.writeNoIO(.stdout, verbose.deleted_entries.items[0..]).asErr()) |err| { - global_handle.get().actuallyThrow(bun.shell.ShellErr.newSys(err)); - return; - } - // _ = this.state.exec.output_done.fetchAdd(1, .SeqCst); - _ = this.state.exec.incrementOutputCount(.output_done); - if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { - this.bltn.done(if (this.state.exec.err != null) 1 else 0); - return; - } - return; - } - this.queueBlockingOutput(verbose.toBlockingOutput()); - } + /// `--time-style=TIME_STYLE` + /// Time/date format with -l + time_style: ?[]const u8 = null, - fn queueBlockingOutput(this: *Rm, bo: BlockingOutput) void { - const node = bun.default_allocator.create(std.DoublyLinkedList(BlockingOutput).Node) catch bun.outOfMemory(); - node.* = .{ - .data = bo, - }; + /// `-t` + /// Sort by time, newest first + sort_by_time: bool = false, - this.state.exec.output_queue.append(node); + /// `-T`, `--tabsize=COLS` + /// Assume tab stops at each specified number of columns + tabsize: ?usize = null, - // Need to start it - if (this.state.exec.output_queue.len == 1) { - this.state.exec.output_queue.first.?.data.writer.writeIfPossible(false); - } - } + /// `-u` + /// Sort by, and show, access time + use_atime: bool = false, - const BlockingOutput = struct { - writer: BufferedWriter, - arr: std.ArrayList(u8), + /// `-U` + /// Do not sort; list entries in directory order + no_sort: bool = false, - pub fn deinit(this: *BlockingOutput) void { - this.arr.deinit(); - } - }; + /// `-v` + /// Natural sort of (version) numbers within text + natural_sort: bool = false, - pub const ShellRmTask = struct { - const print = bun.Output.scoped(.AsyncRmTask, false); + /// `-w`, `--width=COLS` + /// Set output width to specified number of columns + output_width: ?usize = null, - // const MAX_FDS_OPEN: u8 = 16; + /// `-x` + /// List entries by lines instead of by columns + list_by_lines: bool = false, - rm: *Rm, - opts: Opts, + /// `-X` + /// Sort alphabetically by entry extension + sort_by_extension: bool = false, - cwd: bun.FileDescriptor, + /// `-Z`, `--context` + /// Print any security context of each file + show_context: bool = false, - root_task: DirTask, - root_path: bun.PathString = bun.PathString.empty, - root_is_absolute: bool, + /// `--zero` + /// End each output line with NUL, not newline + end_with_nul: bool = false, - // fds_opened: u8 = 0, + /// `-1` + /// List one file per line + one_file_per_line: bool = false, - error_signal: *std.atomic.Value(bool), - err_mutex: bun.Lock = bun.Lock.init(), - err: ?Syscall.Error = null, + /// `--help` + /// Display help and exit + show_help: bool = false, - event_loop: EventLoopRef, - concurrent_task: EventLoopTask = .{}, - task: JSC.WorkPoolTask = .{ - .callback = workPoolCallback, - }, + /// `--version` + /// Output version information and exit + show_version: bool = false, - const ParentRmTask = @This(); - - pub const DirTask = struct { - task_manager: *ParentRmTask, - parent_task: ?*DirTask, - path: [:0]const u8, - subtask_count: std.atomic.Value(usize), - need_to_wait: bool = false, - kind_hint: EntryKindHint, - task: JSC.WorkPoolTask = .{ .callback = runFromThreadPool }, - deleted_entries: std.ArrayList(u8), - concurrent_task: EventLoopTask = .{}, - - const EntryKindHint = enum { idk, dir, file }; - - pub fn toBlockingOutput(this: *DirTask) BlockingOutput { - const arr = this.takeDeletedEntries(); - const bo = BlockingOutput{ - .arr = arr, - .writer = BufferedWriter{ - .fd = bun.STDOUT_FD, - .remain = arr.items[0..], - .parent = BufferedWriter.ParentPtr.init(this.task_manager.rm), - .bytelist = this.task_manager.rm.bltn.stdBufferedBytelist(.stdout), - }, - }; - return bo; - } + /// Custom parse error for invalid options + const ParseError = union(enum) { + illegal_option: []const u8, + show_usage, + }; + }; - pub fn takeDeletedEntries(this: *DirTask) std.ArrayList(u8) { - const ret = this.deleted_entries; - this.deleted_entries = std.ArrayList(u8).init(ret.allocator); - return ret; - } + pub fn parseOpts(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) { + return this.parseFlags(); + } - pub fn runFromMainThread(this: *DirTask) void { - print("runFromMainThread", .{}); - this.task_manager.rm.writeVerbose(this); - } + pub fn parseFlags(this: *Ls) Result(?[]const [*:0]const u8, Opts.ParseError) { + const args = this.bltn.argsSlice(); + var idx: usize = 0; + if (args.len == 0) { + return .{ .ok = null }; + } - pub fn runFromMainThreadMini(this: *DirTask, _: *void) void { - this.runFromMainThread(); - } + while (idx < args.len) : (idx += 1) { + const flag = args[idx]; + switch (this.parseFlag(flag[0..std.mem.len(flag)])) { + .done => { + const filepath_args = args[idx..]; + return .{ .ok = filepath_args }; + }, + .continue_parsing => {}, + .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, + } + } - pub fn runFromThreadPool(task: *JSC.WorkPoolTask) void { - var this: *DirTask = @fieldParentPtr(DirTask, "task", task); - this.runFromThreadPoolImpl(); - } + return .{ .err = .show_usage }; + } - fn runFromThreadPoolImpl(this: *DirTask) void { - defer this.postRun(); - - print("DirTask: {s}", .{this.path}); - switch (this.task_manager.removeEntry(this, ResolvePath.Platform.auto.isAbsolute(this.path[0..this.path.len]))) { - .err => |err| { - print("DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path }); - this.task_manager.err_mutex.lock(); - defer this.task_manager.err_mutex.unlock(); - if (this.task_manager.err == null) { - this.task_manager.err = err; - this.task_manager.error_signal.store(true, .SeqCst); - } else { - bun.default_allocator.free(err.path); - } - }, - .result => {}, - } - } + pub fn parseFlag(this: *Ls, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } { + if (flag.len == 0) return .done; + if (flag[0] != '-') return .done; - fn handleErr(this: *DirTask, err: Syscall.Error) void { - print("DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path }); - this.task_manager.err_mutex.lock(); - defer this.task_manager.err_mutex.unlock(); - if (this.task_manager.err == null) { - this.task_manager.err = err; - this.task_manager.error_signal.store(true, .SeqCst); - } else { - bun.default_allocator.free(err.path); - } - } + // FIXME windows + if (flag.len == 1) return .{ .illegal_option = "-" }; - pub fn postRun(this: *DirTask) void { - // All entries including recursive directories were deleted - if (this.need_to_wait) return; - - // We have executed all the children of this task - if (this.subtask_count.fetchSub(1, .SeqCst) == 1) { - defer { - if (this.task_manager.opts.verbose) - this.queueForWrite() - else - this.deinit(); - } + const small_flags = flag[1..]; + for (small_flags) |char| { + switch (char) { + 'a' => { + this.opts.show_all = true; + }, + 'A' => { + this.opts.show_almost_all = true; + }, + 'b' => { + this.opts.escape = true; + }, + 'B' => { + this.opts.ignore_backups = true; + }, + 'c' => { + this.opts.use_ctime = true; + }, + 'C' => { + this.opts.list_by_columns = true; + }, + 'd' => { + this.opts.list_directories = true; + }, + 'D' => { + this.opts.dired_mode = true; + }, + 'f' => { + this.opts.unsorted = true; + }, + 'F' => { + this.opts.classify = "always"; + }, + 'g' => { + this.opts.no_owner = true; + }, + 'G' => { + this.opts.no_group = true; + }, + 'h' => { + this.opts.human_readable = true; + }, + 'H' => { + this.opts.dereference_cmd_symlinks = true; + }, + 'i' => { + this.opts.show_inode = true; + }, + 'I' => { + this.opts.ignore_pattern = ""; // This will require additional logic to handle patterns + }, + 'k' => { + this.opts.kibibytes = true; + }, + 'l' => { + this.opts.long_listing = true; + }, + 'L' => { + this.opts.dereference = true; + }, + 'm' => { + this.opts.comma_separated = true; + }, + 'n' => { + this.opts.numeric_uid_gid = true; + }, + 'N' => { + this.opts.literal = true; + }, + 'o' => { + this.opts.no_group_info = true; + }, + 'p' => { + this.opts.slash_indicator = true; + }, + 'q' => { + this.opts.hide_control_chars = true; + }, + 'Q' => { + this.opts.quote_name = true; + }, + 'r' => { + this.opts.reverse_order = true; + }, + 'R' => { + this.opts.recursive = true; + }, + 's' => { + this.opts.show_size = true; + }, + 'S' => { + this.opts.sort_by_size = true; + }, + 't' => { + this.opts.sort_by_time = true; + }, + 'T' => { + this.opts.tabsize = 8; // Default tab size, needs additional handling for custom sizes + }, + 'u' => { + this.opts.use_atime = true; + }, + 'U' => { + this.opts.no_sort = true; + }, + 'v' => { + this.opts.natural_sort = true; + }, + 'w' => { + this.opts.output_width = 0; // Default to no limit, needs additional handling for custom widths + }, + 'x' => { + this.opts.list_by_lines = true; + }, + 'X' => { + this.opts.sort_by_extension = true; + }, + 'Z' => { + this.opts.show_context = true; + }, + '1' => { + this.opts.one_file_per_line = true; + }, + else => { + return .{ .illegal_option = flag[1..2] }; + }, + } + } - // If we have a parent and we are the last child, now we can delete the parent - if (this.parent_task != null and this.parent_task.?.subtask_count.fetchSub(1, .SeqCst) == 2) { - this.parent_task.?.deleteAfterWaitingForChildren(); - return; - } + return .continue_parsing; + } + }; - // Otherwise we are root task - this.task_manager.finishConcurrently(); - } + pub const Mv = struct { + bltn: *Builtin, + opts: Opts = .{}, + args: struct { + sources: []const [*:0]const u8 = &[_][*:0]const u8{}, + target: [:0]const u8 = &[0:0]u8{}, + target_fd: ?bun.FileDescriptor = null, + } = .{}, + state: union(enum) { + idle, + check_target: struct { + task: ShellMvCheckTargetTask, + state: union(enum) { + running, + done, + }, + }, + executing: struct { + task_count: usize, + tasks_done: usize = 0, + error_signal: std.atomic.Value(bool), + tasks: []ShellMvBatchedTask, + err: ?Syscall.Error = null, + }, + done, + waiting_write_err: struct { + exit_code: ExitCode, + }, + err, + } = .idle, - // Otherwise need to wait - } + pub const ShellMvCheckTargetTask = struct { + const print = bun.Output.scoped(.MvCheckTargetTask, false); + mv: *Mv, - pub fn deleteAfterWaitingForChildren(this: *DirTask) void { - this.need_to_wait = false; - defer this.postRun(); - if (this.task_manager.error_signal.load(.SeqCst)) { - return; - } + cwd: bun.FileDescriptor, + target: [:0]const u8, + result: ?Maybe(?bun.FileDescriptor) = null, - switch (this.task_manager.removeEntryDirAfterChildren(this)) { - .err => |e| { - print("DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(e.getErrno()), e.path }); - this.task_manager.err_mutex.lock(); - defer this.task_manager.err_mutex.unlock(); - if (this.task_manager.err == null) { - this.task_manager.err = e; - } else { - bun.default_allocator.free(e.path); - } + task: shell.eval.ShellTask(@This(), runFromThreadPool, runFromMainThread, print), + + pub fn runFromThreadPool(this: *@This()) void { + const fd = switch (ShellSyscall.openat(this.cwd, this.target, os.O.RDONLY | os.O.DIRECTORY, 0)) { + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOTDIR => { + this.result = .{ .result = null }; + }, + else => { + this.result = .{ .err = e }; }, - .result => {}, } - } + return; + }, + .result => |fd| fd, + }; + this.result = .{ .result = fd }; + } - pub fn queueForWrite(this: *DirTask) void { - if (this.deleted_entries.items.len == 0) return; - if (comptime EventLoopKind == .js) { - this.task_manager.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } else { - this.task_manager.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, "runFromMainThreadMini")); - } - } + pub fn runFromMainThread(this: *@This()) void { + this.mv.checkTargetTaskDone(this); + } - pub fn deinit(this: *DirTask) void { - this.deleted_entries.deinit(); - // The root's path string is from Rm's argv so don't deallocate it - // And the root task is actually a field on the struct of the AsyncRmTask so don't deallocate it either - if (this.parent_task != null) { - bun.default_allocator.free(this.path); - bun.default_allocator.destroy(this); - } - } - }; + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } + }; - pub fn create(root_path: bun.PathString, rm: *Rm, cwd: bun.FileDescriptor, error_signal: *std.atomic.Value(bool), is_absolute: bool) *ShellRmTask { - const task = bun.default_allocator.create(ShellRmTask) catch bun.outOfMemory(); - task.* = ShellRmTask{ - .rm = rm, - .opts = rm.opts, - .cwd = cwd, - .root_path = root_path, - .root_task = DirTask{ - .task_manager = task, - .parent_task = null, - .path = root_path.sliceAssumeZ(), - .subtask_count = std.atomic.Value(usize).init(1), - .kind_hint = .idk, - .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), - }, - // .event_loop = JSC.VirtualMachine.get().event_loop, - .event_loop = event_loop_ref.get(), - .error_signal = error_signal, - .root_is_absolute = is_absolute, - }; - return task; - } + pub const ShellMvBatchedTask = struct { + const BATCH_SIZE = 5; + const print = bun.Output.scoped(.MvBatchedTask, false); - pub fn schedule(this: *@This()) void { - JSC.WorkPool.schedule(&this.task); - } + mv: *Mv, + sources: []const [*:0]const u8, + target: [:0]const u8, + target_fd: ?bun.FileDescriptor, + cwd: bun.FileDescriptor, + error_signal: *std.atomic.Value(bool), - pub fn enqueue(this: *ShellRmTask, parent_dir: *DirTask, path: [:0]const u8, is_absolute: bool, kind_hint: DirTask.EntryKindHint) void { - if (this.error_signal.load(.SeqCst)) { - return; - } - const new_path = this.join( - bun.default_allocator, - &[_][]const u8{ - parent_dir.path[0..parent_dir.path.len], - path[0..path.len], - }, - is_absolute, - ); - this.enqueueNoJoin(parent_dir, new_path, kind_hint); - } + err: ?Syscall.Error = null, - pub fn enqueueNoJoin(this: *ShellRmTask, parent_task: *DirTask, path: [:0]const u8, kind_hint: DirTask.EntryKindHint) void { - print("enqueue: {s}", .{path}); - if (this.error_signal.load(.SeqCst)) { - return; - } + task: shell.eval.ShellTask(@This(), runFromThreadPool, runFromMainThread, print), + event_loop: JSC.EventLoopHandle, - // if (this.opts.verbose) { - // // _ = this.rm.state.exec.output_count.fetchAdd(1, .SeqCst); - // _ = this.rm.state.exec.incrementOutputCount(.output_count); - // } + pub fn runFromThreadPool(this: *@This()) void { + // Moving multiple entries into a directory + if (this.sources.len > 1) return this.moveMultipleIntoDir(); - var subtask = bun.default_allocator.create(DirTask) catch bun.outOfMemory(); - subtask.* = DirTask{ - .task_manager = this, - .path = path, - .parent_task = parent_task, - .subtask_count = std.atomic.Value(usize).init(1), - .kind_hint = kind_hint, - .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), - }; - std.debug.assert(parent_task.subtask_count.fetchAdd(1, .Monotonic) > 0); - print("enqueue: {s}", .{path}); - JSC.WorkPool.schedule(&subtask.task); - } + const src = this.sources[0][0..std.mem.len(this.sources[0]) :0]; + // Moving entry into directory + if (this.target_fd) |fd| { + _ = fd; - pub fn verboseDeleted(this: *@This(), dir_task: *DirTask, path: [:0]const u8) Maybe(void) { - print("deleted: {s}", .{path[0..path.len]}); - if (!this.opts.verbose) return Maybe(void).success; - if (dir_task.deleted_entries.items.len == 0) { - _ = this.rm.state.exec.incrementOutputCount(.output_count); - } - dir_task.deleted_entries.appendSlice(path[0..path.len]) catch bun.outOfMemory(); - dir_task.deleted_entries.append('\n') catch bun.outOfMemory(); - return Maybe(void).success; + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + _ = this.moveInDir(src, &buf); + return; } - pub fn finishConcurrently(this: *ShellRmTask) void { - if (comptime EventLoopKind == .js) { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); - } else { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, "runFromMainThreadMini")); - } + switch (Syscall.renameat(this.cwd, src, this.cwd, this.target)) { + .err => |e| { + this.err = e; + }, + else => {}, } + } - pub fn bufJoin(buf: *[bun.MAX_PATH_BYTES]u8, parts: []const []const u8, syscall_tag: Syscall.Tag) Maybe([:0]u8) { - var fixed_buf_allocator = std.heap.FixedBufferAllocator.init(buf[0..]); - return .{ .result = std.fs.path.joinZ(fixed_buf_allocator.allocator(), parts) catch return Maybe([:0]u8).initErr(Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, syscall_tag)) }; - } + pub fn moveInDir(this: *@This(), src: [:0]const u8, buf: *[bun.MAX_PATH_BYTES]u8) bool { + var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]); - pub fn removeEntry(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool) Maybe(void) { - var buf: [bun.MAX_PATH_BYTES]u8 = undefined; - switch (dir_task.kind_hint) { - .idk, .file => return this.removeEntryFile(dir_task, dir_task.path, is_absolute, &buf, false), - .dir => return this.removeEntryDir(dir_task, is_absolute, &buf), - } + const path_in_dir = std.fs.path.joinZ(fixed_alloc.allocator(), &[_][]const u8{ + "./", + ResolvePath.basename(src), + }) catch { + this.err = Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, .rename); + return false; + }; + + switch (Syscall.renameat(this.cwd, src, this.target_fd.?, path_in_dir)) { + .err => |e| { + const target_path = ResolvePath.joinZ(&[_][]const u8{ + this.target, + ResolvePath.basename(src), + }, .auto); + + this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory()); + return false; + }, + else => {}, } - fn removeEntryDir(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { - const path = dir_task.path; - const dirfd = this.cwd; + return true; + } - // If `-d` is specified without `-r` then we can just use `rmdirat` - if (this.opts.remove_empty_dirs and !this.opts.recursive) { - switch (Syscall.rmdirat(dirfd, path)) { - .result => return Maybe(void).success, - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) return this.verboseDeleted(dir_task, path); - return .{ .err = this.errorWithPath(e, path) }; - }, - bun.C.E.NOTDIR => { - return this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, false); - }, - else => return .{ .err = this.errorWithPath(e, path) }, - } - }, - } - } + fn moveMultipleIntoDir(this: *@This()) void { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var fixed_alloc = std.heap.FixedBufferAllocator.init(buf[0..bun.MAX_PATH_BYTES]); - if (!this.opts.recursive) { - return Maybe(void).initErr(Syscall.Error.fromCode(bun.C.E.ISDIR, .TODO).withPath(bun.default_allocator.dupeZ(u8, dir_task.path) catch bun.outOfMemory())); - } + for (this.sources) |src_raw| { + if (this.error_signal.load(.SeqCst)) return; + defer fixed_alloc.reset(); - const flags = os.O.DIRECTORY | os.O.RDONLY; - const fd = switch (Syscall.openat(dirfd, path, flags, 0)) { - .result => |fd| fd, - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) return this.verboseDeleted(dir_task, path); - return .{ .err = this.errorWithPath(e, path) }; - }, - bun.C.E.NOTDIR => { - return this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, false); - }, - else => return .{ .err = this.errorWithPath(e, path) }, - } - }, - }; - defer { - _ = Syscall.close(fd); + const src = src_raw[0..std.mem.len(src_raw) :0]; + if (!this.moveInDir(src, &buf)) { + return; } + } + } - if (this.error_signal.load(.SeqCst)) { - return Maybe(void).success; - } + /// From the man pages of `mv`: + /// ```txt + /// As the rename(2) call does not work across file systems, mv uses cp(1) and rm(1) to accomplish the move. The effect is equivalent to: + /// rm -f destination_path && \ + /// cp -pRP source_file destination && \ + /// rm -rf source_file + /// ``` + fn moveAcrossFilesystems(this: *@This(), src: [:0]const u8, dest: [:0]const u8) void { + _ = this; + _ = src; + _ = dest; - var iterator = DirIterator.iterate(fd.asDir(), .u8); - var entry = iterator.next(); + // TODO + } - var i: usize = 0; - while (switch (entry) { - .err => |err| { - return .{ .err = this.errorWithPath(err, path) }; - }, - .result => |ent| ent, - }) |current| : (entry = iterator.next()) { - // TODO this seems bad maybe better to listen to kqueue/epoll event - if (fastMod(i, 4) == 0 and this.error_signal.load(.SeqCst)) return Maybe(void).success; + pub fn runFromMainThread(this: *@This()) void { + this.mv.batchedMoveTaskDone(this); + } - defer i += 1; - switch (current.kind) { - .directory => { - this.enqueue(dir_task, current.name.sliceAssumeZ(), is_absolute, .dir); - }, - else => { - const name = current.name.sliceAssumeZ(); - const file_path = switch (ShellRmTask.bufJoin( - buf, - &[_][]const u8{ - path[0..path.len], - name[0..name.len], - }, - .unlink, - )) { - .err => |e| return .{ .err = e }, - .result => |p| p, - }; + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } + }; - switch (this.removeEntryFile(dir_task, file_path, is_absolute, buf, true)) { - .err => |e| return .{ .err = this.errorWithPath(e, current.name.sliceAssumeZ()) }, - .result => {}, - } - }, - } - } + pub fn start(this: *Mv) Maybe(void) { + return this.next(); + } - // Need to wait for children to finish - if (dir_task.subtask_count.load(.SeqCst) > 1) { - dir_task.need_to_wait = true; - return Maybe(void).success; - } + pub fn writeFailingError(this: *Mv, buf: []const u8, exit_code: ExitCode) Maybe(void) { + if (this.bltn.stderr.needsIO()) { + this.state = .{ .waiting_write_err = .{ .exit_code = exit_code } }; + this.bltn.stderr.enqueue(this, buf); + return Maybe(void).success; + } - if (this.error_signal.load(.SeqCst)) return Maybe(void).success; + _ = this.bltn.writeNoIO(.stderr, buf); - switch (Syscall.unlinkatWithFlags(dirfd, path, std.os.AT.REMOVEDIR)) { - .result => { - switch (this.verboseDeleted(dir_task, path)) { - .err => |e| return .{ .err = e }, - else => {}, - } - return Maybe(void).success; - }, - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) { - switch (this.verboseDeleted(dir_task, path)) { - .err => |e2| return .{ .err = e2 }, - else => {}, - } - return Maybe(void).success; - } + this.bltn.done(exit_code); + return Maybe(void).success; + } - return .{ .err = this.errorWithPath(e, path) }; - }, - else => return .{ .err = e }, - } - }, - } - } + pub fn next(this: *Mv) Maybe(void) { + while (!(this.state == .done or this.state == .err)) { + switch (this.state) { + .idle => { + if (this.parseOpts().asErr()) |e| { + const buf = switch (e) { + .illegal_option => |opt_str| this.bltn.fmtErrorArena(.mv, "illegal option -- {s}\n", .{opt_str}), + .show_usage => Builtin.Kind.mv.usageString(), + }; - fn removeEntryDirAfterChildren(this: *ShellRmTask, dir_task: *DirTask) Maybe(void) { - const dirfd = bun.toFD(this.cwd); - var treat_as_dir = true; - const fd: bun.FileDescriptor = handle_entry: while (true) { - if (treat_as_dir) { - switch (Syscall.openat(dirfd, dir_task.path, os.O.DIRECTORY | os.O.RDONLY, 0)) { - .err => |e| switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) { - if (this.verboseDeleted(dir_task, dir_task.path).asErr()) |e2| return .{ .err = e2 }; - return Maybe(void).success; - } - return .{ .err = e }; - }, - bun.C.E.NOTDIR => { - treat_as_dir = false; - continue; + return this.writeFailingError(buf, 1); + } + this.state = .{ + .check_target = .{ + .task = ShellMvCheckTargetTask{ + .mv = this, + .cwd = this.bltn.parentCmd().base.shell.cwd_fd, + .target = this.args.target, + .task = .{ + // .event_loop = JSC.VirtualMachine.get().eventLoop(), + .event_loop = this.bltn.parentCmd().base.eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn.parentCmd().base.eventLoop()), }, - else => return .{ .err = e }, }, - .result => |fd| break :handle_entry fd, - } - } else { - if (Syscall.unlinkat(dirfd, dir_task.path).asErr()) |e| { + .state = .running, + }, + }; + this.state.check_target.task.task.schedule(); + return Maybe(void).success; + }, + .check_target => { + if (this.state.check_target.state == .running) return Maybe(void).success; + const check_target = &this.state.check_target; + + if (comptime bun.Environment.allow_assert) { + std.debug.assert(check_target.task.result != null); + } + + const maybe_fd: ?bun.FileDescriptor = switch (check_target.task.result.?) { + .err => |e| brk: { + defer bun.default_allocator.free(e.path); switch (e.getErrno()) { bun.C.E.NOENT => { - if (this.opts.force) { - if (this.verboseDeleted(dir_task, dir_task.path).asErr()) |e2| return .{ .err = e2 }; - return Maybe(void).success; - } - return .{ .err = e }; - }, - bun.C.E.ISDIR => { - treat_as_dir = true; - continue; + // Means we are renaming entry, not moving to a directory + if (this.args.sources.len == 1) break :brk null; + + const buf = this.bltn.fmtErrorArena(.mv, "{s}: No such file or directory\n", .{this.args.target}); + return this.writeFailingError(buf, 1); }, - bun.C.E.PERM => { - // TODO should check if dir - return .{ .err = e }; + else => { + const sys_err = e.toSystemError(); + const buf = this.bltn.fmtErrorArena(.mv, "{s}: {s}\n", .{ sys_err.path.byteSlice(), sys_err.message.byteSlice() }); + return this.writeFailingError(buf, 1); }, - else => return .{ .err = e }, } - } - return Maybe(void).success; + }, + .result => |maybe_fd| maybe_fd, + }; + + // Trying to move multiple files into a file + if (maybe_fd == null and this.args.sources.len > 1) { + const buf = this.bltn.fmtErrorArena(.mv, "{s} is not a directory\n", .{this.args.target}); + return this.writeFailingError(buf, 1); } - }; - defer { - _ = Syscall.close(fd); - } + const task_count = brk: { + const sources_len: f64 = @floatFromInt(this.args.sources.len); + const batch_size: f64 = @floatFromInt(ShellMvBatchedTask.BATCH_SIZE); + const task_count: usize = @intFromFloat(@ceil(sources_len / batch_size)); + break :brk task_count; + }; - switch (Syscall.unlinkatWithFlags(dirfd, dir_task.path, std.os.AT.REMOVEDIR)) { - .result => { - switch (this.verboseDeleted(dir_task, dir_task.path)) { - .err => |e| return .{ .err = e }, - else => {}, - } - return Maybe(void).success; - }, - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) { - if (this.verboseDeleted(dir_task, dir_task.path).asErr()) |e2| return .{ .err = e2 }; - return Maybe(void).success; - } - return .{ .err = e }; - }, - else => return .{ .err = e }, + this.args.target_fd = maybe_fd; + const cwd_fd = this.bltn.parentCmd().base.shell.cwd_fd; + const tasks = this.bltn.arena.allocator().alloc(ShellMvBatchedTask, task_count) catch bun.outOfMemory(); + // Initialize tasks + { + var count = task_count; + const count_per_task = this.args.sources.len / ShellMvBatchedTask.BATCH_SIZE; + var i: usize = 0; + var j: usize = 0; + while (i < tasks.len -| 1) : (i += 1) { + j += count_per_task; + const sources = this.args.sources[j .. j + count_per_task]; + count -|= count_per_task; + tasks[i] = ShellMvBatchedTask{ + .mv = this, + .cwd = cwd_fd, + .target = this.args.target, + .target_fd = this.args.target_fd, + .sources = sources, + // We set this later + .error_signal = undefined, + .task = .{ + .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn.parentCmd().base.eventLoop()), + .event_loop = this.bltn.parentCmd().base.eventLoop(), + }, + .event_loop = this.bltn.parentCmd().base.eventLoop(), + }; } - }, - } - } - fn removeEntryFile( - this: *ShellRmTask, - parent_dir_task: *DirTask, - path: [:0]const u8, - is_absolute: bool, - buf: *[bun.MAX_PATH_BYTES]u8, - comptime is_file_in_dir: bool, - ) Maybe(void) { - const dirfd = bun.toFD(this.cwd); - switch (Syscall.unlinkatWithFlags(dirfd, path, 0)) { - .result => return this.verboseDeleted(parent_dir_task, path), - .err => |e| { - switch (e.getErrno()) { - bun.C.E.NOENT => { - if (this.opts.force) - return this.verboseDeleted(parent_dir_task, path); + // Give remainder to last task + if (count > 0) { + const sources = this.args.sources[j .. j + count]; + tasks[i] = ShellMvBatchedTask{ + .mv = this, + .cwd = cwd_fd, + .target = this.args.target, + .target_fd = this.args.target_fd, + .sources = sources, + // We set this later + .error_signal = undefined, + .task = .{ + .event_loop = this.bltn.parentCmd().base.eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.bltn.parentCmd().base.eventLoop()), + }, + .event_loop = this.bltn.parentCmd().base.eventLoop(), + }; + } + } - return .{ .err = this.errorWithPath(e, path) }; - }, - bun.C.E.ISDIR => { - if (comptime is_file_in_dir) { - this.enqueueNoJoin(parent_dir_task, path, .dir); - return Maybe(void).success; - } - return this.removeEntryDir(parent_dir_task, is_absolute, buf); - }, - // This might happen if the file is actually a directory - bun.C.E.PERM => { - switch (builtin.os.tag) { - // non-Linux POSIX systems return EPERM when trying to delete a directory, so - // we need to handle that case specifically and translate the error - .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { - // If we are allowed to delete directories then we can call `unlink`. - // If `path` points to a directory, then it is deleted (if empty) or we handle it as a directory - // If it's actually a file, we get an error so we don't need to call `stat` to check that. - if (this.opts.recursive or this.opts.remove_empty_dirs) { - return switch (Syscall.unlinkatWithFlags(dirfd, path, std.os.AT.REMOVEDIR)) { - // it was empty, we saved a syscall - .result => return this.verboseDeleted(parent_dir_task, path), - .err => |e2| { - return switch (e2.getErrno()) { - // not empty, process directory as we would normally - bun.C.E.NOTEMPTY => { - this.enqueueNoJoin(parent_dir_task, path, .dir); - return Maybe(void).success; - }, - // actually a file, the error is a permissions error - bun.C.E.NOTDIR => .{ .err = this.errorWithPath(e, path) }, - else => .{ .err = this.errorWithPath(e2, path) }, - }; - }, - }; - } + this.state = .{ + .executing = .{ + .task_count = task_count, + .error_signal = std.atomic.Value(bool).init(false), + .tasks = tasks, + }, + }; - // We don't know if it was an actual permissions error or it was a directory so we need to try to delete it as a directory - if (comptime is_file_in_dir) { - this.enqueueNoJoin(parent_dir_task, path, .dir); - return Maybe(void).success; - } - return this.removeEntryDir(parent_dir_task, is_absolute, buf); - }, - else => {}, - } + for (this.state.executing.tasks) |*t| { + t.error_signal = &this.state.executing.error_signal; + t.task.schedule(); + } - return .{ .err = this.errorWithPath(e, path) }; - }, - else => return .{ .err = this.errorWithPath(e, path) }, - } - }, - } + return Maybe(void).success; + }, + .executing => { + const exec = &this.state.executing; + _ = exec; + // if (exec.state == .idle) { + // // 1. Check if target is directory or file + // } + }, + .waiting_write_err => { + return Maybe(void).success; + }, + .done, .err => unreachable, } + } - fn errorWithPath(this: *ShellRmTask, err: Syscall.Error, path: [:0]const u8) Syscall.Error { - _ = this; - return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory()); - } + if (this.state == .done) { + this.bltn.done(0); + return Maybe(void).success; + } + + this.bltn.done(1); + return Maybe(void).success; + } - inline fn join(this: *ShellRmTask, alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 { - _ = this; - if (!is_absolute) { - // If relative paths enabled, stdlib join is preferred over - // ResolvePath.joinBuf because it doesn't try to normalize the path - return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory(); + pub fn onIOWriterChunk(this: *Mv, e: ?JSC.SystemError) void { + defer if (e) |err| err.deref(); + switch (this.state) { + .waiting_write_err => { + if (e != null) { + this.state = .err; + _ = this.next(); + return; } + this.bltn.done(this.state.waiting_write_err.exit_code); + return; + }, + else => @panic("Invalid state"), + } + } - const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory(); + pub fn checkTargetTaskDone(this: *Mv, task: *ShellMvCheckTargetTask) void { + _ = task; - return out; - } + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .check_target); + std.debug.assert(this.state.check_target.task.result != null); + } - pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { - var this: *ShellRmTask = @fieldParentPtr(ShellRmTask, "task", task); - this.root_task.runFromThreadPoolImpl(); - } + this.state.check_target.state = .done; + _ = this.next(); + return; + } - pub fn runFromMainThread(this: *ShellRmTask) void { - this.rm.onAsyncTaskDone(this); - } + pub fn batchedMoveTaskDone(this: *Mv, task: *ShellMvBatchedTask) void { + if (comptime bun.Environment.allow_assert) { + std.debug.assert(this.state == .executing); + std.debug.assert(this.state.executing.tasks_done < this.state.executing.task_count); + } + + var exec = &this.state.executing; - pub fn runFromMainThreadMini(this: *ShellRmTask, _: *void) void { - this.rm.onAsyncTaskDone(this); + if (task.err) |err| { + exec.error_signal.store(true, .SeqCst); + if (exec.err == null) { + exec.err = err; + } else { + bun.default_allocator.free(err.path); } + } - pub fn deinit(this: *ShellRmTask) void { - bun.default_allocator.destroy(this); + exec.tasks_done += 1; + if (exec.tasks_done >= exec.task_count) { + if (exec.err) |err| { + const buf = this.bltn.fmtErrorArena(.ls, "{s}\n", .{err.toSystemError().message.byteSlice()}); + _ = this.writeFailingError(buf, err.errno); + return; } - }; - }; - }; + this.state = .done; - /// This is modified version of BufferedInput for file descriptors only. - /// - /// This struct cleans itself up when it is done, so no need to call `.deinit()` on - /// it. IT DOES NOT CLOSE FILE DESCRIPTORS - pub const BufferedWriter = - struct { - remain: []const u8 = "", - fd: bun.FileDescriptor, - poll_ref: ?*bun.Async.FilePoll = null, - written: usize = 0, - parent: ParentPtr, - err: ?Syscall.Error = null, - /// optional bytelist for capturing the data - bytelist: ?*bun.ByteList = null, + _ = this.next(); + return; + } + } - const print = bun.Output.scoped(.BufferedWriter, false); - const CmdJs = bun.shell.Interpreter.Cmd; - const CmdMini = bun.shell.InterpreterMini.Cmd; - const PipelineJs = bun.shell.Interpreter.Pipeline; - const PipelineMini = bun.shell.InterpreterMini.Pipeline; - const BuiltinJs = bun.shell.Interpreter.Builtin; - const BuiltinMini = bun.shell.InterpreterMini.Builtin; - - pub const ParentPtr = struct { - const Types = .{ - BuiltinJs.Export, - BuiltinJs.Echo, - BuiltinJs.Cd, - BuiltinJs.Which, - BuiltinJs.Rm, - BuiltinJs.Pwd, - BuiltinJs.Mv, - BuiltinJs.Ls, - BuiltinMini.Export, - BuiltinMini.Echo, - BuiltinMini.Cd, - BuiltinMini.Which, - BuiltinMini.Rm, - BuiltinMini.Pwd, - BuiltinMini.Mv, - BuiltinMini.Ls, - CmdJs, - CmdMini, - PipelineJs, - PipelineMini, + pub fn deinit(this: *Mv) void { + if (this.args.target_fd != null and this.args.target_fd.? != bun.invalid_fd) { + _ = Syscall.close(this.args.target_fd.?); + } + } + + const Opts = struct { + /// `-f` + /// + /// Do not prompt for confirmation before overwriting the destination path. (The -f option overrides any previous -i or -n options.) + force_overwrite: bool = true, + /// `-h` + /// + /// If the target operand is a symbolic link to a directory, do not follow it. This causes the mv utility to rename the file source to the destination path target rather than moving source into the + /// directory referenced by target. + no_dereference: bool = false, + /// `-i` + /// + /// Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file. If the response from the standard input begins with the character ‘y’ or ‘Y’, the move is + /// attempted. (The -i option overrides any previous -f or -n options.) + interactive_mode: bool = false, + /// `-n` + /// + /// Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.) + no_overwrite: bool = false, + /// `-v` + /// + /// Cause mv to be verbose, showing files after they are moved. + verbose_output: bool = false, + + const ParseError = union(enum) { + illegal_option: []const u8, + show_usage, }; - ptr: Repr, - pub const Repr = TaggedPointerUnion(Types); + }; - pub fn underlying(this: ParentPtr) type { - inline for (Types) |Ty| { - if (this.ptr.is(Ty)) return Ty; - } - @panic("Uh oh"); - } + pub fn parseOpts(this: *Mv) Result(void, Opts.ParseError) { + const filepath_args = switch (this.parseFlags()) { + .ok => |args| args, + .err => |e| return .{ .err = e }, + }; - pub fn init(p: anytype) ParentPtr { - return .{ - .ptr = Repr.init(p), - }; + if (filepath_args.len < 2) { + return .{ .err = .show_usage }; } - pub fn onDone(this: ParentPtr, e: ?Syscall.Error) void { - if (this.ptr.is(BuiltinJs.Export)) return this.ptr.as(BuiltinJs.Export).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Echo)) return this.ptr.as(BuiltinJs.Echo).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Cd)) return this.ptr.as(BuiltinJs.Cd).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Which)) return this.ptr.as(BuiltinJs.Which).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Rm)) return this.ptr.as(BuiltinJs.Rm).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Pwd)) return this.ptr.as(BuiltinJs.Pwd).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Mv)) return this.ptr.as(BuiltinJs.Mv).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinJs.Ls)) return this.ptr.as(BuiltinJs.Ls).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Export)) return this.ptr.as(BuiltinMini.Export).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Echo)) return this.ptr.as(BuiltinMini.Echo).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Cd)) return this.ptr.as(BuiltinMini.Cd).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Which)) return this.ptr.as(BuiltinMini.Which).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Rm)) return this.ptr.as(BuiltinMini.Rm).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Pwd)) return this.ptr.as(BuiltinMini.Pwd).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Mv)) return this.ptr.as(BuiltinMini.Mv).onBufferedWriterDone(e); - if (this.ptr.is(BuiltinMini.Ls)) return this.ptr.as(BuiltinMini.Ls).onBufferedWriterDone(e); - if (this.ptr.is(CmdJs)) return this.ptr.as(CmdJs).onBufferedWriterDone(e); - if (this.ptr.is(CmdMini)) return this.ptr.as(CmdMini).onBufferedWriterDone(e); - @panic("Invalid ptr tag"); - } - }; + this.args.sources = filepath_args[0 .. filepath_args.len - 1]; + this.args.target = filepath_args[filepath_args.len - 1][0..std.mem.len(filepath_args[filepath_args.len - 1]) :0]; - pub fn isDone(this: *BufferedWriter) bool { - return this.remain.len == 0 or this.err != null; + return .ok; } - pub const event_loop_kind = EventLoopKind; - pub usingnamespace JSC.WebCore.NewReadyWatcher(BufferedWriter, .writable, onReady); + pub fn parseFlags(this: *Mv) Result([]const [*:0]const u8, Opts.ParseError) { + const args = this.bltn.argsSlice(); + var idx: usize = 0; + if (args.len == 0) { + return .{ .err = .show_usage }; + } - pub fn onReady(this: *BufferedWriter, _: i64) void { - if (this.fd == bun.invalid_fd) { - return; + while (idx < args.len) : (idx += 1) { + const flag = args[idx]; + switch (this.parseFlag(flag[0..std.mem.len(flag)])) { + .done => { + const filepath_args = args[idx..]; + return .{ .ok = filepath_args }; + }, + .continue_parsing => {}, + .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, + } } - this.__write(); + return .{ .err = .show_usage }; } - pub fn writeIfPossible(this: *BufferedWriter, comptime is_sync: bool) void { - if (this.remain.len == 0) return this.deinit(); - if (comptime !is_sync) { - // we ask, "Is it possible to write right now?" - // we do this rather than epoll or kqueue() - // because we don't want to block the thread waiting for the write - switch (bun.isWritable(this.fd)) { - .ready => { - if (this.poll_ref) |poll| { - poll.flags.insert(.writable); - poll.flags.insert(.fifo); - std.debug.assert(poll.flags.contains(.poll_writable)); - } + pub fn parseFlag(this: *Mv, flag: []const u8) union(enum) { continue_parsing, done, illegal_option: []const u8 } { + if (flag.len == 0) return .done; + if (flag[0] != '-') return .done; + + const small_flags = flag[1..]; + for (small_flags) |char| { + switch (char) { + 'f' => { + this.opts.force_overwrite = true; + this.opts.interactive_mode = false; + this.opts.no_overwrite = false; }, - .hup => { - this.deinit(); - return; + 'h' => { + this.opts.no_dereference = true; }, - .not_ready => { - if (!this.isWatching()) this.watch(this.fd); - return; + 'i' => { + this.opts.interactive_mode = true; + this.opts.force_overwrite = false; + this.opts.no_overwrite = false; + }, + 'n' => { + this.opts.no_overwrite = true; + this.opts.force_overwrite = false; + this.opts.interactive_mode = false; + }, + 'v' => { + this.opts.verbose_output = true; + }, + else => { + return .{ .illegal_option = "-" }; }, } } - this.writeAllowBlocking(is_sync); + return .continue_parsing; } + }; - /// Calling this directly will block if the fd is not opened with non - /// blocking option. If the fd is blocking, you should call - /// `writeIfPossible()` first, which will check if the fd is writable. If so - /// it will then call this function, if not, then it will poll for the fd to - /// be writable - pub fn __write(this: *BufferedWriter) void { - this.writeAllowBlocking(false); - } - - pub fn writeAllowBlocking(this: *BufferedWriter, allow_blocking: bool) void { - var to_write = this.remain; - - if (to_write.len == 0) { - // we are done! - this.deinit(); - return; - } - + pub const Rm = struct { + bltn: *Builtin, + opts: Opts, + state: union(enum) { + idle, + parse_opts: struct { + args_slice: []const [*:0]const u8, + idx: u32 = 0, + state: union(enum) { + normal, + wait_write_err, + } = .normal, + }, + exec: struct { + // task: RmTask, + filepath_args: []const [*:0]const u8, + total_tasks: usize, + err: ?Syscall.Error = null, + lock: std.Thread.Mutex = std.Thread.Mutex{}, + error_signal: std.atomic.Value(bool) = .{ .raw = false }, + output_done: std.atomic.Value(usize) = .{ .raw = 0 }, + output_count: std.atomic.Value(usize) = .{ .raw = 0 }, + state: union(enum) { + idle, + waiting: struct { + tasks_done: usize = 0, + }, + + pub fn tasksDone(this: *@This()) usize { + return switch (this.*) { + .idle => 0, + .waiting => this.waiting.tasks_done, + }; + } + }, + + fn incrementOutputCount(this: *@This(), comptime thevar: @Type(.EnumLiteral)) void { + @fence(.SeqCst); + var atomicvar = &@field(this, @tagName(thevar)); + const result = atomicvar.fetchAdd(1, .SeqCst); + log("[rm] {s}: {d} + 1", .{ @tagName(thevar), result }); + return; + } + + fn getOutputCount(this: *@This(), comptime thevar: @Type(.EnumLiteral)) usize { + @fence(.SeqCst); + var atomicvar = &@field(this, @tagName(thevar)); + return atomicvar.load(.SeqCst); + } + }, + done: struct { exit_code: ExitCode }, + err: ExitCode, + } = .idle, + + pub const Opts = struct { + /// `--no-preserve-root` / `--preserve-root` + /// + /// If set to false, then allow the recursive removal of the root directory. + /// Safety feature to prevent accidental deletion of the root directory. + preserve_root: bool = true, + + /// `-f`, `--force` + /// + /// Ignore nonexistent files and arguments, never prompt. + force: bool = false, + + /// Configures how the user should be prompted on removal of files. + prompt_behaviour: PromptBehaviour = .never, + + /// `-r`, `-R`, `--recursive` + /// + /// Remove directories and their contents recursively. + recursive: bool = false, + + /// `-v`, `--verbose` + /// + /// Explain what is being done (prints which files/dirs are being deleted). + verbose: bool = false, + + /// `-d`, `--dir` + /// + /// Remove empty directories. This option permits you to remove a directory + /// without specifying `-r`/`-R`/`--recursive`, provided that the directory is + /// empty. + remove_empty_dirs: bool = false, + + const PromptBehaviour = union(enum) { + /// `--interactive=never` + /// + /// Default + never, + + /// `-I`, `--interactive=once` + /// + /// Once before removing more than three files, or when removing recursively. + once: struct { + removed_count: u32 = 0, + }, + + /// `-i`, `--interactive=always` + /// + /// Prompt before every removal. + always, + }; + }; + + pub fn start(this: *Rm) Maybe(void) { + return this.next(); + } + + pub noinline fn next(this: *Rm) Maybe(void) { + while (this.state != .done and this.state != .err) { + switch (this.state) { + .idle => { + this.state = .{ + .parse_opts = .{ + .args_slice = this.bltn.argsSlice(), + }, + }; + continue; + }, + .parse_opts => { + var parse_opts = &this.state.parse_opts; + switch (parse_opts.state) { + .normal => { + // This means there were no arguments or only + // flag arguments meaning no positionals, in + // either case we must print the usage error + // string + if (parse_opts.idx >= parse_opts.args_slice.len) { + const error_string = Builtin.Kind.usageString(.rm); + if (this.bltn.stderr.needsIO()) { + parse_opts.state = .wait_write_err; + this.bltn.stderr.enqueue(this, error_string); + return Maybe(void).success; + } + + _ = this.bltn.writeNoIO(.stderr, error_string); + + this.bltn.done(1); + return Maybe(void).success; + } + + const idx = parse_opts.idx; + + const arg_raw = parse_opts.args_slice[idx]; + const arg = arg_raw[0..std.mem.len(arg_raw)]; + + switch (parseFlag(&this.opts, this.bltn, arg)) { + .continue_parsing => { + parse_opts.idx += 1; + continue; + }, + .done => { + if (this.opts.recursive) { + this.opts.remove_empty_dirs = true; + } + + if (this.opts.prompt_behaviour != .never) { + const buf = "rm: \"-i\" is not supported yet"; + if (this.bltn.stderr.needsIO()) { + parse_opts.state = .wait_write_err; + this.bltn.stderr.enqueue(this, buf); + continue; + } + + _ = this.bltn.writeNoIO(.stderr, buf); + + this.bltn.done(1); + return Maybe(void).success; + } + + const filepath_args_start = idx; + const filepath_args = parse_opts.args_slice[filepath_args_start..]; + + // Check that non of the paths will delete the root + { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd = switch (Syscall.getcwd(&buf)) { + .err => |err| { + return .{ .err = err }; + }, + .result => |cwd| cwd, + }; + + for (filepath_args) |filepath| { + const path = filepath[0..bun.len(filepath)]; + const resolved_path = if (ResolvePath.Platform.auto.isAbsolute(path)) path else bun.path.join(&[_][]const u8{ cwd, path }, .auto); + const is_root = brk: { + const normalized = bun.path.normalizeString(resolved_path, false, .auto); + const dirname = ResolvePath.dirname(normalized, .auto); + const is_root = std.mem.eql(u8, dirname, ""); + break :brk is_root; + }; + + if (is_root) { + if (this.bltn.stderr.needsIO()) { + parse_opts.state = .wait_write_err; + this.bltn.stderr.enqueueFmtBltn(this, .rm, "\"{s}\" may not be removed\n", .{resolved_path}); + return Maybe(void).success; + } + + const error_string = this.bltn.fmtErrorArena(.rm, "\"{s}\" may not be removed\n", .{resolved_path}); + + _ = this.bltn.writeNoIO(.stderr, error_string); + + this.bltn.done(1); + return Maybe(void).success; + } + } + } + + const total_tasks = filepath_args.len; + this.state = .{ + .exec = .{ + .filepath_args = filepath_args, + .total_tasks = total_tasks, + .state = .idle, + .output_done = std.atomic.Value(usize).init(0), + .output_count = std.atomic.Value(usize).init(0), + }, + }; + // this.state.exec.task.schedule(); + // return Maybe(void).success; + continue; + }, + .illegal_option => { + const error_string = "rm: illegal option -- -\n"; + if (this.bltn.stderr.needsIO()) { + parse_opts.state = .wait_write_err; + this.bltn.stderr.enqueue(this, error_string); + return Maybe(void).success; + } + + _ = this.bltn.writeNoIO(.stderr, error_string); + + this.bltn.done(1); + return Maybe(void).success; + }, + .illegal_option_with_flag => { + const flag = arg; + if (this.bltn.stderr.needsIO()) { + parse_opts.state = .wait_write_err; + this.bltn.stderr.enqueueFmtBltn(this, .rm, "illegal option -- {s}\n", .{flag[1..]}); + return Maybe(void).success; + } + const error_string = this.bltn.fmtErrorArena(.rm, "illegal option -- {s}\n", .{flag[1..]}); + + _ = this.bltn.writeNoIO(.stderr, error_string); + + this.bltn.done(1); + return Maybe(void).success; + }, + } + }, + .wait_write_err => { + @panic("Invalid"); + // // Errored + // if (parse_opts.state.wait_write_err.err) |e| { + // this.state = .{ .err = e }; + // continue; + // } + + // // Done writing + // if (this.state.parse_opts.state.wait_write_err.remain() == 0) { + // this.state = .{ .done = .{ .exit_code = 0 } }; + // continue; + // } + + // // yield execution to continue writing + // return Maybe(void).success; + }, + } + }, + .exec => { + const cwd = this.bltn.parentCmd().base.shell.cwd_fd; + // Schedule task + if (this.state.exec.state == .idle) { + this.state.exec.state = .{ .waiting = .{} }; + for (this.state.exec.filepath_args) |root_raw| { + const root = root_raw[0..std.mem.len(root_raw)]; + const root_path_string = bun.PathString.init(root[0..root.len]); + const is_absolute = ResolvePath.Platform.auto.isAbsolute(root); + var task = ShellRmTask.create(root_path_string, this, cwd, &this.state.exec.error_signal, is_absolute); + task.schedule(); + // task. + } + } + + // do nothing + return Maybe(void).success; + }, + .done, .err => unreachable, + } + } + + if (this.state == .done) { + this.bltn.done(0); + return Maybe(void).success; + } + + if (this.state == .err) { + this.bltn.done(this.state.err); + return Maybe(void).success; + } + + return Maybe(void).success; + } + + pub fn onIOWriterChunk(this: *Rm, e: ?JSC.SystemError) void { if (comptime bun.Environment.allow_assert) { - // bun.assertNonBlocking(this.fd); + std.debug.assert((this.state == .parse_opts and this.state.parse_opts.state == .wait_write_err) or + (this.state == .exec and this.state.exec.state == .waiting and this.state.exec.output_count.load(.SeqCst) > 0)); } - while (to_write.len > 0) { - switch (bun.sys.write(this.fd, to_write)) { - .err => |e| { - if (e.isRetry()) { - log("write({d}) retry", .{ - to_write.len, - }); + if (this.state == .exec and this.state.exec.state == .waiting) { + log("[rm] output done={d} output count={d}", .{ this.state.exec.getOutputCount(.output_done), this.state.exec.getOutputCount(.output_count) }); + this.state.exec.incrementOutputCount(.output_done); + if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { + const code: ExitCode = if (this.state.exec.err != null) 1 else 0; + this.bltn.done(code); + return; + } + return; + } + + if (e != null) { + defer e.?.deref(); + this.state = .{ .err = @intFromEnum(e.?.getErrno()) }; + this.bltn.done(e.?.getErrno()); + return; + } + + this.bltn.done(1); + return; + } + + // pub fn writeToStdoutFromAsyncTask(this: *Rm, comptime fmt: []const u8, args: anytype) Maybe(void) { + // const buf = this.rm.bltn.fmtErrorArena(null, fmt, args); + // if (!this.rm.bltn.stdout.needsIO()) { + // this.state.exec.lock.lock(); + // defer this.state.exec.lock.unlock(); + // _ = this.rm.bltn.writeNoIO(.stdout, buf); + // return Maybe(void).success; + // } + + // var written: usize = 0; + // while (written < buf.len) : (written += switch (Syscall.write(this.rm.bltn.stdout.fd, buf)) { + // .err => |e| return Maybe(void).initErr(e), + // .result => |n| n, + // }) {} + + // return Maybe(void).success; + // } + + pub fn deinit(this: *Rm) void { + _ = this; + } + + const ParseFlagsResult = enum { + continue_parsing, + done, + illegal_option, + illegal_option_with_flag, + }; + + fn parseFlag(this: *Opts, bltn: *Builtin, flag: []const u8) ParseFlagsResult { + _ = bltn; + if (flag.len == 0) return .done; + if (flag[0] != '-') return .done; + if (flag.len > 2 and flag[1] == '-') { + if (bun.strings.eqlComptime(flag, "--preserve-root")) { + this.preserve_root = true; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--no-preserve-root")) { + this.preserve_root = false; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--recursive")) { + this.recursive = true; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--verbose")) { + this.verbose = true; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--dir")) { + this.remove_empty_dirs = true; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--interactive=never")) { + this.prompt_behaviour = .never; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--interactive=once")) { + this.prompt_behaviour = .{ .once = .{} }; + return .continue_parsing; + } else if (bun.strings.eqlComptime(flag, "--interactive=always")) { + this.prompt_behaviour = .always; + return .continue_parsing; + } + + // try bltn.write_err(&bltn.stderr, .rm, "illegal option -- -\n", .{}); + return .illegal_option; + } + + const small_flags = flag[1..]; + for (small_flags) |char| { + switch (char) { + 'f' => { + this.force = true; + this.prompt_behaviour = .never; + }, + 'r', 'R' => { + this.recursive = true; + }, + 'v' => { + this.verbose = true; + }, + 'd' => { + this.remove_empty_dirs = true; + }, + 'i' => { + this.prompt_behaviour = .{ .once = .{} }; + }, + 'I' => { + this.prompt_behaviour = .always; + }, + else => { + // try bltn.write_err(&bltn.stderr, .rm, "illegal option -- {s}\n", .{flag[1..]}); + return .illegal_option_with_flag; + }, + } + } - this.watch(this.fd); - this.poll_ref.?.flags.insert(.fifo); + return .continue_parsing; + } + + pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void { + var exec = &this.state.exec; + const tasks_done = switch (exec.state) { + .idle => @panic("Invalid state"), + .waiting => brk: { + exec.state.waiting.tasks_done += 1; + const amt = exec.state.waiting.tasks_done; + if (task.err) |err| { + exec.err = err; + const error_string = this.bltn.taskErrorToString(.rm, err); + if (!this.bltn.stderr.needsIO()) { + _ = this.bltn.writeNoIO(.stderr, error_string); + } else { + exec.incrementOutputCount(.output_count); + this.bltn.stderr.enqueue(this, error_string); return; } + } + break :brk amt; + }, + }; + + log("ShellRmTask(0x{x}, task.)", .{task.root_path}); + // Wait until all tasks done and all output is written + if (tasks_done >= this.state.exec.total_tasks and + exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count)) + { + this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } }; + _ = this.next(); + return; + } + } + + fn writeVerbose(this: *Rm, verbose: *ShellRmTask.DirTask) void { + if (!this.bltn.stdout.needsIO()) { + _ = this.bltn.writeNoIO(.stdout, verbose.deleted_entries.items[0..]); + // _ = this.state.exec.output_done.fetchAdd(1, .SeqCst); + _ = this.state.exec.incrementOutputCount(.output_done); + if (this.state.exec.state.tasksDone() >= this.state.exec.total_tasks and this.state.exec.getOutputCount(.output_done) >= this.state.exec.getOutputCount(.output_count)) { + this.bltn.done(if (this.state.exec.err != null) @as(ExitCode, 1) else @as(ExitCode, 0)); + return; + } + return; + } + const buf = verbose.takeDeletedEntries(); + defer buf.deinit(); + this.bltn.stdout.enqueue(this, buf.items[0..]); + } + + pub const ShellRmTask = struct { + const print = bun.Output.scoped(.AsyncRmTask, false); + + // const MAX_FDS_OPEN: u8 = 16; + + rm: *Rm, + opts: Opts, + + cwd: bun.FileDescriptor, + cwd_path: ?CwdPath = if (bun.Environment.isPosix) 0 else null, + + root_task: DirTask, + root_path: bun.PathString = bun.PathString.empty, + root_is_absolute: bool, + + // fds_opened: u8 = 0, + + error_signal: *std.atomic.Value(bool), + err_mutex: bun.Lock = bun.Lock.init(), + err: ?Syscall.Error = null, + + event_loop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, + task: JSC.WorkPoolTask = .{ + .callback = workPoolCallback, + }, + + const CwdPath = if (bun.Environment.isWindows) [:0]const u8 else u0; + + const ParentRmTask = @This(); + + pub const DirTask = struct { + task_manager: *ParentRmTask, + parent_task: ?*DirTask, + path: [:0]const u8, + is_absolute: bool = false, + subtask_count: std.atomic.Value(usize), + need_to_wait: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + kind_hint: EntryKindHint, + task: JSC.WorkPoolTask = .{ .callback = runFromThreadPool }, + deleted_entries: std.ArrayList(u8), + concurrent_task: JSC.EventLoopTask, + + const EntryKindHint = enum { idk, dir, file }; + + pub fn takeDeletedEntries(this: *DirTask) std.ArrayList(u8) { + const ret = this.deleted_entries; + this.deleted_entries = std.ArrayList(u8).init(ret.allocator); + return ret; + } + + pub fn runFromMainThread(this: *DirTask) void { + print("DirTask(0x{x}, path={s}) runFromMainThread", .{ @intFromPtr(this), this.path }); + this.task_manager.rm.writeVerbose(this); + } + + pub fn runFromMainThreadMini(this: *DirTask, _: *void) void { + this.runFromMainThread(); + } + + pub fn runFromThreadPool(task: *JSC.WorkPoolTask) void { + var this: *DirTask = @fieldParentPtr(DirTask, "task", task); + this.runFromThreadPoolImpl(); + } + + fn runFromThreadPoolImpl(this: *DirTask) void { + defer this.postRun(); + + // Root, get cwd path on windows + if (bun.Environment.isWindows) { + if (this.parent_task == null) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd_path = switch (Syscall.getFdPath(this.task_manager.cwd, &buf)) { + .result => |p| bun.default_allocator.dupeZ(u8, p) catch bun.outOfMemory(), + .err => |err| { + print("[runFromThreadPoolImpl:getcwd] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path }); + this.task_manager.err_mutex.lock(); + defer this.task_manager.err_mutex.unlock(); + if (this.task_manager.err == null) { + this.task_manager.err = err; + this.task_manager.error_signal.store(true, .SeqCst); + } + return; + }, + }; + this.task_manager.cwd_path = cwd_path; + } + } + + print("DirTask: {s}", .{this.path}); + this.is_absolute = ResolvePath.Platform.auto.isAbsolute(this.path[0..this.path.len]); + switch (this.task_manager.removeEntry(this, this.is_absolute)) { + .err => |err| { + print("[runFromThreadPoolImpl] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path }); + this.task_manager.err_mutex.lock(); + defer this.task_manager.err_mutex.unlock(); + if (this.task_manager.err == null) { + this.task_manager.err = err; + this.task_manager.error_signal.store(true, .SeqCst); + } else { + bun.default_allocator.free(err.path); + } + }, + .result => {}, + } + } + + fn handleErr(this: *DirTask, err: Syscall.Error) void { + print("[handleErr] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(err.getErrno()), err.path }); + this.task_manager.err_mutex.lock(); + defer this.task_manager.err_mutex.unlock(); + if (this.task_manager.err == null) { + this.task_manager.err = err; + this.task_manager.error_signal.store(true, .SeqCst); + } else { + bun.default_allocator.free(err.path); + } + } + + pub fn postRun(this: *DirTask) void { + // // This is true if the directory has subdirectories + // // that need to be deleted + if (this.need_to_wait.load(.SeqCst)) return; + + // We have executed all the children of this task + if (this.subtask_count.fetchSub(1, .SeqCst) == 1) { + defer { + if (this.task_manager.opts.verbose) + this.queueForWrite() + else + this.deinit(); + } - if (e.getErrno() == .PIPE) { - this.deinit(); + // If we have a parent and we are the last child, now we can delete the parent + if (this.parent_task != null) { + // It's possible that we queued this subdir task and it finished, while the parent + // was still in the `removeEntryDir` function + const tasks_left_before_decrement = this.parent_task.?.subtask_count.fetchSub(1, .SeqCst); + const parent_still_in_remove_entry_dir = !this.parent_task.?.need_to_wait.load(.Monotonic); + if (!parent_still_in_remove_entry_dir and tasks_left_before_decrement == 2) { + this.parent_task.?.deleteAfterWaitingForChildren(); + } return; } - // fail - log("write({d}) fail: {d}", .{ to_write.len, e.errno }); - this.err = e; - this.deinit(); + // Otherwise we are root task + this.task_manager.finishConcurrently(); + } + + // Otherwise need to wait + } + + pub fn deleteAfterWaitingForChildren(this: *DirTask) void { + this.need_to_wait.store(false, .SeqCst); + var do_post_run = true; + defer { + if (do_post_run) this.postRun(); + } + if (this.task_manager.error_signal.load(.SeqCst)) { return; - }, + } - .result => |bytes_written| { - if (this.bytelist) |blist| { - blist.append(bun.default_allocator, to_write[0..bytes_written]) catch bun.outOfMemory(); - } + switch (this.task_manager.removeEntryDirAfterChildren(this)) { + .err => |e| { + print("[deleteAfterWaitingForChildren] DirTask({x}) failed: {s}: {s}", .{ @intFromPtr(this), @tagName(e.getErrno()), e.path }); + this.task_manager.err_mutex.lock(); + defer this.task_manager.err_mutex.unlock(); + if (this.task_manager.err == null) { + this.task_manager.err = e; + } else { + bun.default_allocator.free(e.path); + } + }, + .result => |deleted| { + if (!deleted) { + do_post_run = false; + } + }, + } + } - this.written += bytes_written; + pub fn queueForWrite(this: *DirTask) void { + if (this.deleted_entries.items.len == 0) return; + if (this.task_manager.event_loop == .js) { + this.task_manager.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.task_manager.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } - log( - "write({d}) {d}", - .{ - to_write.len, - bytes_written, - }, - ); + pub fn deinit(this: *DirTask) void { + this.deleted_entries.deinit(); + // The root's path string is from Rm's argv so don't deallocate it + // And the root task is actually a field on the struct of the AsyncRmTask so don't deallocate it either + if (this.parent_task != null) { + bun.default_allocator.free(this.path); + bun.default_allocator.destroy(this); + } + } + }; + + pub fn create(root_path: bun.PathString, rm: *Rm, cwd: bun.FileDescriptor, error_signal: *std.atomic.Value(bool), is_absolute: bool) *ShellRmTask { + const task = bun.default_allocator.create(ShellRmTask) catch bun.outOfMemory(); + task.* = ShellRmTask{ + .rm = rm, + .opts = rm.opts, + .cwd = cwd, + .root_path = root_path, + .root_task = DirTask{ + .task_manager = task, + .parent_task = null, + .path = root_path.sliceAssumeZ(), + .subtask_count = std.atomic.Value(usize).init(1), + .kind_hint = .idk, + .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn.eventLoop()), + }, + .event_loop = rm.bltn.parentCmd().base.eventLoop(), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(rm.bltn.eventLoop()), + .error_signal = error_signal, + .root_is_absolute = is_absolute, + }; + return task; + } + + pub fn schedule(this: *@This()) void { + JSC.WorkPool.schedule(&this.task); + } + + pub fn enqueue(this: *ShellRmTask, parent_dir: *DirTask, path: [:0]const u8, is_absolute: bool, kind_hint: DirTask.EntryKindHint) void { + if (this.error_signal.load(.SeqCst)) { + return; + } + const new_path = this.join( + bun.default_allocator, + &[_][]const u8{ + parent_dir.path[0..parent_dir.path.len], + path[0..path.len], + }, + is_absolute, + ); + this.enqueueNoJoin(parent_dir, new_path, kind_hint); + } + + pub fn enqueueNoJoin(this: *ShellRmTask, parent_task: *DirTask, path: [:0]const u8, kind_hint: DirTask.EntryKindHint) void { + defer print("enqueue: {s} {s}", .{ path, @tagName(kind_hint) }); + + if (this.error_signal.load(.SeqCst)) { + return; + } + + // if (this.opts.verbose) { + // // _ = this.rm.state.exec.output_count.fetchAdd(1, .SeqCst); + // _ = this.rm.state.exec.incrementOutputCount(.output_count); + // } + + var subtask = bun.default_allocator.create(DirTask) catch bun.outOfMemory(); + subtask.* = DirTask{ + .task_manager = this, + .path = path, + .parent_task = parent_task, + .subtask_count = std.atomic.Value(usize).init(1), + .kind_hint = kind_hint, + .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), + .concurrent_task = JSC.EventLoopTask.fromEventLoop(this.event_loop), + }; + std.debug.assert(parent_task.subtask_count.fetchAdd(1, .Monotonic) > 0); + + JSC.WorkPool.schedule(&subtask.task); + } + + pub fn getcwd(this: *ShellRmTask) if (bun.Environment.isWindows) CwdPath else bun.FileDescriptor { + return if (bun.Environment.isWindows) this.cwd_path.? else bun.toFD(this.cwd); + } + + pub fn verboseDeleted(this: *@This(), dir_task: *DirTask, path: [:0]const u8) Maybe(void) { + print("deleted: {s}", .{path[0..path.len]}); + if (!this.opts.verbose) return Maybe(void).success; + if (dir_task.deleted_entries.items.len == 0) { + _ = this.rm.state.exec.incrementOutputCount(.output_count); + } + dir_task.deleted_entries.appendSlice(path[0..path.len]) catch bun.outOfMemory(); + dir_task.deleted_entries.append('\n') catch bun.outOfMemory(); + return Maybe(void).success; + } + + pub fn finishConcurrently(this: *ShellRmTask) void { + print("finishConcurrently", .{}); + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit)); + } else { + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } + + pub fn bufJoin(buf: *[bun.MAX_PATH_BYTES]u8, parts: []const []const u8, syscall_tag: Syscall.Tag) Maybe([:0]u8) { + var fixed_buf_allocator = std.heap.FixedBufferAllocator.init(buf[0..]); + return .{ .result = std.fs.path.joinZ(fixed_buf_allocator.allocator(), parts) catch return Maybe([:0]u8).initErr(Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, syscall_tag)) }; + } + + pub fn removeEntry(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool) Maybe(void) { + var remove_child_vtable = RemoveFileVTable{ + .task = this, + .child_of_dir = false, + }; + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + switch (dir_task.kind_hint) { + .idk, .file => return this.removeEntryFile(dir_task, dir_task.path, is_absolute, &buf, &remove_child_vtable), + .dir => return this.removeEntryDir(dir_task, is_absolute, &buf), + } + } + + fn removeEntryDir(this: *ShellRmTask, dir_task: *DirTask, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + const path = dir_task.path; + const dirfd = this.cwd; + print("removeEntryDir({s})", .{path}); + + // If `-d` is specified without `-r` then we can just use `rmdirat` + if (this.opts.remove_empty_dirs and !this.opts.recursive) out_to_iter: { + var delete_state = RemoveFileParent{ + .task = this, + .treat_as_dir = true, + .allow_enqueue = false, + }; + while (delete_state.treat_as_dir) { + switch (ShellSyscall.rmdirat(dirfd, path)) { + .result => return Maybe(void).success, + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOENT => { + if (this.opts.force) return this.verboseDeleted(dir_task, path); + return .{ .err = this.errorWithPath(e, path) }; + }, + bun.C.E.NOTDIR => { + delete_state.treat_as_dir = false; + if (this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &delete_state).asErr()) |err| { + return .{ .err = this.errorWithPath(err, path) }; + } + if (!delete_state.treat_as_dir) return Maybe(void).success; + if (delete_state.treat_as_dir) break :out_to_iter; + }, + else => return .{ .err = this.errorWithPath(e, path) }, + } + }, + } + } + } + + if (!this.opts.recursive) { + return Maybe(void).initErr(Syscall.Error.fromCode(bun.C.E.ISDIR, .TODO).withPath(bun.default_allocator.dupeZ(u8, dir_task.path) catch bun.outOfMemory())); + } + + const flags = os.O.DIRECTORY | os.O.RDONLY; + const fd = switch (ShellSyscall.openat(dirfd, path, flags, 0)) { + .result => |fd| fd, + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOENT => { + if (this.opts.force) return this.verboseDeleted(dir_task, path); + return .{ .err = this.errorWithPath(e, path) }; + }, + bun.C.E.NOTDIR => { + return this.removeEntryFile(dir_task, dir_task.path, is_absolute, buf, &DummyRemoveFile.dummy); + }, + else => return .{ .err = this.errorWithPath(e, path) }, + } + }, + }; + + var close_fd = true; + defer { + // On posix we can close the file descriptor whenever, but on Windows + // we need to close it BEFORE we delete + if (close_fd) { + _ = Syscall.close(fd); + } + } + + if (this.error_signal.load(.SeqCst)) { + return Maybe(void).success; + } + + var iterator = DirIterator.iterate(fd.asDir(), .u8); + var entry = iterator.next(); + + var remove_child_vtable = RemoveFileVTable{ + .task = this, + .child_of_dir = true, + }; + + var i: usize = 0; + while (switch (entry) { + .err => |err| { + return .{ .err = this.errorWithPath(err, path) }; + }, + .result => |ent| ent, + }) |current| : (entry = iterator.next()) { + print("dir({s}) entry({s}, {s})", .{ path, current.name.slice(), @tagName(current.kind) }); + // TODO this seems bad maybe better to listen to kqueue/epoll event + if (fastMod(i, 4) == 0 and this.error_signal.load(.SeqCst)) return Maybe(void).success; + + defer i += 1; + switch (current.kind) { + .directory => { + this.enqueue(dir_task, current.name.sliceAssumeZ(), is_absolute, .dir); + }, + else => { + const name = current.name.sliceAssumeZ(); + const file_path = switch (ShellRmTask.bufJoin( + buf, + &[_][]const u8{ + path[0..path.len], + name[0..name.len], + }, + .unlink, + )) { + .err => |e| return .{ .err = e }, + .result => |p| p, + }; + + switch (this.removeEntryFile(dir_task, file_path, is_absolute, buf, &remove_child_vtable)) { + .err => |e| return .{ .err = this.errorWithPath(e, current.name.sliceAssumeZ()) }, + .result => {}, + } + }, + } + } + + // Need to wait for children to finish + if (dir_task.subtask_count.load(.SeqCst) > 1) { + close_fd = true; + dir_task.need_to_wait.store(true, .SeqCst); + return Maybe(void).success; + } + + if (this.error_signal.load(.SeqCst)) return Maybe(void).success; + + if (bun.Environment.isWindows) { + close_fd = false; + _ = Syscall.close(fd); + } + + print("[removeEntryDir] remove after children {s}", .{path}); + switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.os.AT.REMOVEDIR)) { + .result => { + switch (this.verboseDeleted(dir_task, path)) { + .err => |e| return .{ .err = e }, + else => {}, + } + return Maybe(void).success; + }, + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOENT => { + if (this.opts.force) { + switch (this.verboseDeleted(dir_task, path)) { + .err => |e2| return .{ .err = e2 }, + else => {}, + } + return Maybe(void).success; + } + + return .{ .err = this.errorWithPath(e, path) }; + }, + else => return .{ .err = e }, + } + }, + } + } + + const DummyRemoveFile = struct { + var dummy: @This() = std.mem.zeroes(@This()); + + pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + _ = this; // autofix + _ = parent_dir_task; // autofix + _ = path; // autofix + _ = is_absolute; // autofix + _ = buf; // autofix + + return Maybe(void).success; + } + + pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + _ = this; // autofix + _ = parent_dir_task; // autofix + _ = path; // autofix + _ = is_absolute; // autofix + _ = buf; // autofix + + return Maybe(void).success; + } + }; + + const RemoveFileVTable = struct { + task: *ShellRmTask, + child_of_dir: bool, + + pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + if (this.child_of_dir) { + this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir); + return Maybe(void).success; + } + return this.task.removeEntryDir(parent_dir_task, is_absolute, buf); + } + + pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + if (this.child_of_dir) return .{ .result = this.task.enqueueNoJoin(parent_dir_task, bun.default_allocator.dupeZ(u8, path) catch bun.outOfMemory(), .dir) }; + return this.task.removeEntryDir(parent_dir_task, is_absolute, buf); + } + }; + + const RemoveFileParent = struct { + task: *ShellRmTask, + treat_as_dir: bool, + allow_enqueue: bool = true, + enqueued: bool = false, + + pub fn onIsDir(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + _ = parent_dir_task; // autofix + _ = path; // autofix + _ = is_absolute; // autofix + _ = buf; // autofix + + this.treat_as_dir = true; + return Maybe(void).success; + } + + pub fn onDirNotEmpty(this: *@This(), parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + _ = is_absolute; // autofix + _ = buf; // autofix + + this.treat_as_dir = true; + if (this.allow_enqueue) { + this.task.enqueueNoJoin(parent_dir_task, path, .dir); + this.enqueued = true; + } + return Maybe(void).success; + } + }; + + fn removeEntryDirAfterChildren(this: *ShellRmTask, dir_task: *DirTask) Maybe(bool) { + print("remove entry after children: {s}", .{dir_task.path}); + const dirfd = bun.toFD(this.cwd); + var state = RemoveFileParent{ + .task = this, + .treat_as_dir = true, + }; + while (true) { + if (state.treat_as_dir) { + log("rmdirat({}, {s})", .{ dirfd, dir_task.path }); + switch (ShellSyscall.rmdirat(dirfd, dir_task.path)) { + .result => { + _ = this.verboseDeleted(dir_task, dir_task.path); + return .{ .result = true }; + }, + .err => |e| { + switch (e.getErrno()) { + bun.C.E.NOENT => { + if (this.opts.force) { + _ = this.verboseDeleted(dir_task, dir_task.path); + return .{ .result = true }; + } + return .{ .err = this.errorWithPath(e, dir_task.path) }; + }, + bun.C.E.NOTDIR => { + state.treat_as_dir = false; + continue; + }, + else => return .{ .err = this.errorWithPath(e, dir_task.path) }, + } + }, + } + } else { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + if (this.removeEntryFile(dir_task, dir_task.path, dir_task.is_absolute, &buf, &state).asErr()) |e| { + return .{ .err = e }; + } + if (state.enqueued) return .{ .result = false }; + if (state.treat_as_dir) continue; + return .{ .result = true }; + } + } + } + + fn removeEntryFile(this: *ShellRmTask, parent_dir_task: *DirTask, path: [:0]const u8, is_absolute: bool, buf: *[bun.MAX_PATH_BYTES]u8, vtable: anytype) Maybe(void) { + const VTable = std.meta.Child(@TypeOf(vtable)); + const Handler = struct { + pub fn onIsDir(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + if (@hasDecl(VTable, "onIsDir")) { + return VTable.onIsDir(vtable_, parent_dir_task_, path_, is_absolute_, buf_); + } + return Maybe(void).success; + } + + pub fn onDirNotEmpty(vtable_: anytype, parent_dir_task_: *DirTask, path_: [:0]const u8, is_absolute_: bool, buf_: *[bun.MAX_PATH_BYTES]u8) Maybe(void) { + if (@hasDecl(VTable, "onDirNotEmpty")) { + return VTable.onDirNotEmpty(vtable_, parent_dir_task_, path_, is_absolute_, buf_); + } + return Maybe(void).success; + } + }; + const dirfd = bun.toFD(this.cwd); + _ = dirfd; // autofix + switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, 0)) { + .result => return this.verboseDeleted(parent_dir_task, path), + .err => |e| { + print("unlinkatWithFlags({s}) = {s}", .{ path, @tagName(e.getErrno()) }); + switch (e.getErrno()) { + bun.C.E.NOENT => { + if (this.opts.force) + return this.verboseDeleted(parent_dir_task, path); + + return .{ .err = this.errorWithPath(e, path) }; + }, + bun.C.E.ISDIR => { + return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf); + // if (comptime is_file_in_dir) { + // this.enqueueNoJoin(parent_dir_task, path, .dir); + // return Maybe(void).success; + // } + // return this.removeEntryDir(parent_dir_task, is_absolute, buf); + }, + // This might happen if the file is actually a directory + bun.C.E.PERM => { + switch (builtin.os.tag) { + // non-Linux POSIX systems and Windows return EPERM when trying to delete a directory, so + // we need to handle that case specifically and translate the error + .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos, .windows => { + // If we are allowed to delete directories then we can call `unlink`. + // If `path` points to a directory, then it is deleted (if empty) or we handle it as a directory + // If it's actually a file, we get an error so we don't need to call `stat` to check that. + if (this.opts.recursive or this.opts.remove_empty_dirs) { + return switch (ShellSyscall.unlinkatWithFlags(this.getcwd(), path, std.os.AT.REMOVEDIR)) { + // it was empty, we saved a syscall + .result => return this.verboseDeleted(parent_dir_task, path), + .err => |e2| { + return switch (e2.getErrno()) { + // not empty, process directory as we would normally + bun.C.E.NOTEMPTY => { + // this.enqueueNoJoin(parent_dir_task, path, .dir); + // return Maybe(void).success; + return Handler.onDirNotEmpty(vtable, parent_dir_task, path, is_absolute, buf); + }, + // actually a file, the error is a permissions error + bun.C.E.NOTDIR => .{ .err = this.errorWithPath(e, path) }, + else => .{ .err = this.errorWithPath(e2, path) }, + }; + }, + }; + } + + // We don't know if it was an actual permissions error or it was a directory so we need to try to delete it as a directory + return Handler.onIsDir(vtable, parent_dir_task, path, is_absolute, buf); + }, + else => {}, + } + + return .{ .err = this.errorWithPath(e, path) }; + }, + else => return .{ .err = this.errorWithPath(e, path) }, + } + }, + } + } + + fn errorWithPath(this: *ShellRmTask, err: Syscall.Error, path: [:0]const u8) Syscall.Error { + _ = this; + return err.withPath(bun.default_allocator.dupeZ(u8, path[0..path.len]) catch bun.outOfMemory()); + } + + inline fn join(this: *ShellRmTask, alloc: Allocator, subdir_parts: []const []const u8, is_absolute: bool) [:0]const u8 { + _ = this; + if (!is_absolute) { + // If relative paths enabled, stdlib join is preferred over + // ResolvePath.joinBuf because it doesn't try to normalize the path + return std.fs.path.joinZ(alloc, subdir_parts) catch bun.outOfMemory(); + } + + const out = alloc.dupeZ(u8, bun.path.join(subdir_parts, .auto)) catch bun.outOfMemory(); + + return out; + } + + pub fn workPoolCallback(task: *JSC.WorkPoolTask) void { + var this: *ShellRmTask = @fieldParentPtr(ShellRmTask, "task", task); + this.root_task.runFromThreadPoolImpl(); + } + + pub fn runFromMainThread(this: *ShellRmTask) void { + this.rm.onShellRmTaskDone(this); + } + + pub fn runFromMainThreadMini(this: *ShellRmTask, _: *void) void { + this.rm.onShellRmTaskDone(this); + } + + pub fn deinit(this: *ShellRmTask) void { + bun.default_allocator.destroy(this); + } + }; + }; + }; + + /// This type is reference counted, but deinitialization is queued onto the event loop + pub const IOReader = struct { + fd: bun.FileDescriptor, + reader: ReaderImpl, + buf: std.ArrayListUnmanaged(u8) = .{}, + readers: Readers = .{ .inlined = .{} }, + read: usize = 0, + ref_count: u32 = 1, + err: ?JSC.SystemError = null, + evtloop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, + async_deinit: AsyncDeinit, + is_reading: if (bun.Environment.isWindows) bool else u0 = if (bun.Environment.isWindows) false else 0, + + pub const ChildPtr = IOReaderChildPtr; + pub const ReaderImpl = bun.io.BufferedReader; + + pub const DEBUG_REFCOUNT_NAME: []const u8 = "IOReaderRefCount"; + pub usingnamespace bun.NewRefCounted(@This(), IOReader.asyncDeinit); + + pub fn refSelf(this: *IOReader) *IOReader { + this.ref(); + return this; + } + + pub fn eventLoop(this: *IOReader) JSC.EventLoopHandle { + return this.evtloop; + } + + pub fn loop(this: *IOReader) *bun.uws.Loop { + return this.evtloop.loop(); + } + + pub fn init(fd: bun.FileDescriptor, evtloop: JSC.EventLoopHandle) *IOReader { + const this = IOReader.new(.{ + .fd = fd, + .reader = ReaderImpl.init(@This()), + .evtloop = evtloop, + .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop), + .async_deinit = .{}, + }); + log("IOReader(0x{x}, fd={}) create", .{ @intFromPtr(this), fd }); + + if (bun.Environment.isPosix) { + this.reader.flags.close_handle = false; + } + + if (bun.Environment.isWindows) { + this.reader.source = .{ .file = bun.io.Source.openFile(fd) }; + } + this.reader.setParent(this); + + return this; + } + + /// Idempotent function to start the reading + pub fn start(this: *IOReader) void { + if (bun.Environment.isPosix) { + if (this.reader.handle == .closed or !this.reader.handle.poll.isRegistered()) { + if (this.reader.start(this.fd, true).asErr()) |_| { + @panic("TODO handle error"); + } + } + return; + } + + if (this.is_reading) return; + this.is_reading = true; + if (this.reader.startWithCurrentPipe().asErr()) |e| { + _ = e; + @panic("TODO handle error"); + } + } + + /// Only does things on windows + pub inline fn setReading(this: *IOReader, reading: bool) void { + if (bun.Environment.isWindows) { + log("IOReader(0x{x}) setReading({any})", .{ @intFromPtr(this), reading }); + this.is_reading = reading; + } + } + + pub fn addReader(this: *IOReader, reader_: anytype) void { + const reader: ChildPtr = switch (@TypeOf(reader_)) { + ChildPtr => reader_, + else => ChildPtr.init(reader_), + }; + + const slice = this.readers.slice(); + const usize_slice: []const usize = @as([*]const usize, @ptrCast(slice.ptr))[0..slice.len]; + const ptr_usize: usize = @intFromPtr(reader.ptr.ptr()); + // Only add if it hasn't been added yet + if (std.mem.indexOfScalar(usize, usize_slice, ptr_usize) == null) { + this.readers.append(reader); + } + } + + pub fn removeReader(this: *IOReader, reader_: anytype) void { + const reader = switch (@TypeOf(reader_)) { + ChildPtr => reader_, + else => ChildPtr.init(reader_), + }; + const slice = this.readers.slice(); + const usize_slice: []const usize = @as([*]const usize, @ptrCast(slice.ptr))[0..slice.len]; + const ptr_usize: usize = @intFromPtr(reader.ptr.ptr()); + if (std.mem.indexOfScalar(usize, usize_slice, ptr_usize)) |idx| { + this.readers.swapRemove(idx); + } + } + + pub fn onReadChunk(ptr: *anyopaque, chunk: []const u8, has_more: bun.io.ReadState) bool { + var this: *IOReader = @ptrCast(@alignCast(ptr)); + log("IOReader(0x{x}, fd={}) onReadChunk(chunk_len={d}, has_more={s})", .{ @intFromPtr(this), this.fd, chunk.len, @tagName(has_more) }); + this.setReading(false); + + var i: usize = 0; + while (i < this.readers.len()) { + var r = this.readers.get(i); + switch (r.onReadChunk(chunk)) { + .cont => { + i += 1; + }, + .stop_listening => { + this.readers.swapRemove(i); + }, + } + } + + const should_continue = has_more != .eof; + if (should_continue) { + if (this.readers.len() > 0) { + this.setReading(true); + if (bun.Environment.isPosix) + this.reader.registerPoll() + else switch (this.reader.startWithCurrentPipe()) { + .err => |e| { + const writer = std.io.getStdOut().writer(); + e.format("Yoops ", .{}, writer) catch @panic("oops"); + @panic("TODO SHELL SUBPROC onReadChunk error"); + }, + else => {}, + } + } + } + + return should_continue; + } + + pub fn onReaderError(this: *IOReader, err: bun.sys.Error) void { + this.setReading(false); + this.err = err.toSystemError(); + for (this.readers.slice()) |r| { + r.onReaderDone(if (this.err) |*e| brk: { + e.ref(); + break :brk e.*; + } else null); + } + } + + pub fn onReaderDone(this: *IOReader) void { + log("IOReader(0x{x}) done", .{@intFromPtr(this)}); + this.setReading(false); + for (this.readers.slice()) |r| { + r.onReaderDone(if (this.err) |*err| brk: { + err.ref(); + break :brk err.*; + } else null); + } + } + + pub fn asyncDeinit(this: *@This()) void { + log("IOReader(0x{x}) asyncDeinit", .{@intFromPtr(this)}); + this.async_deinit.schedule(); + } + + pub fn __deinit(this: *@This()) void { + if (this.fd != bun.invalid_fd) { + // windows reader closes the file descriptor + if (bun.Environment.isWindows) { + if (this.reader.source != null and !this.reader.source.?.isClosed()) { + this.reader.closeImpl(false); + } + } else { + log("IOReader(0x{x}) __deinit fd={}", .{ @intFromPtr(this), this.fd }); + _ = bun.sys.close(this.fd); + } + } + this.buf.deinit(bun.default_allocator); + this.reader.disableKeepingProcessAlive({}); + this.reader.deinit(); + bun.destroy(this); + } + + pub const Reader = struct { + ptr: ChildPtr, + }; + + pub const Readers = SmolList(ChildPtr, 4); + }; + + pub const AsyncDeinitWriter = struct { + task: WorkPoolTask = .{ .callback = &runFromThreadPool }, + + pub fn runFromThreadPool(task: *WorkPoolTask) void { + var this = @fieldParentPtr(@This(), "task", task); + var iowriter = this.writer(); + if (iowriter.evtloop == .js) { + iowriter.evtloop.js.enqueueTaskConcurrent(iowriter.concurrent_task.js.from(this, .manual_deinit)); + } else { + iowriter.evtloop.mini.enqueueTaskConcurrent(iowriter.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } + + pub fn writer(this: *@This()) *IOWriter { + return @fieldParentPtr(IOWriter, "async_deinit", this); + } + + pub fn runFromMainThread(this: *@This()) void { + const ioreader = @fieldParentPtr(IOWriter, "async_deinit", this); + ioreader.__deinit(); + } + + pub fn runFromMainThreadMini(this: *@This(), _: *void) void { + this.runFromMainThread(); + } + + pub fn schedule(this: *@This()) void { + WorkPool.schedule(&this.task); + } + }; + + pub const AsyncDeinit = struct { + task: WorkPoolTask = .{ .callback = &runFromThreadPool }, + + pub fn runFromThreadPool(task: *WorkPoolTask) void { + var this = @fieldParentPtr(AsyncDeinit, "task", task); + var ioreader = this.reader(); + if (ioreader.evtloop == .js) { + ioreader.evtloop.js.enqueueTaskConcurrent(ioreader.concurrent_task.js.from(this, .manual_deinit)); + } else { + ioreader.evtloop.mini.enqueueTaskConcurrent(ioreader.concurrent_task.mini.from(this, "runFromMainThreadMini")); + } + } + + pub fn reader(this: *AsyncDeinit) *IOReader { + return @fieldParentPtr(IOReader, "async_deinit", this); + } + + pub fn runFromMainThread(this: *AsyncDeinit) void { + const ioreader = @fieldParentPtr(IOReader, "async_deinit", this); + ioreader.__deinit(); + } + + pub fn runFromMainThreadMini(this: *AsyncDeinit, _: *void) void { + this.runFromMainThread(); + } + + pub fn schedule(this: *AsyncDeinit) void { + WorkPool.schedule(&this.task); + } + }; + + pub const IOWriter = struct { + writer: WriterImpl = if (bun.Environment.isWindows) .{} else .{ + .close_fd = false, + }, + fd: bun.FileDescriptor, + writers: Writers = .{ .inlined = .{} }, + buf: std.ArrayListUnmanaged(u8) = .{}, + __idx: usize = 0, + total_bytes_written: usize = 0, + ref_count: u32 = 1, + err: ?JSC.SystemError = null, + evtloop: JSC.EventLoopHandle, + concurrent_task: JSC.EventLoopTask, + is_writing: if (bun.Environment.isWindows) bool else u0 = if (bun.Environment.isWindows) false else 0, + async_deinit: AsyncDeinitWriter = .{}, + + pub const DEBUG_REFCOUNT_NAME: []const u8 = "IOWriterRefCount"; + + const print = bun.Output.scoped(.IOWriter, false); + + const ChildPtr = IOWriterChildPtr; + // const ChildPtr = anyopaque{}; + + /// ~128kb + /// We shrunk the `buf` when we reach the last writer, + /// but if this never happens, we shrink `buf` when it exceeds this threshold + const SHRINK_THRESHOLD = 1024 * 128; + + pub const auto_poll = false; + + usingnamespace bun.NewRefCounted(@This(), asyncDeinit); + const This = @This(); + pub const WriterImpl = bun.io.BufferedWriter( + This, + onWrite, + onError, + onClose, + getBuffer, + null, + ); + pub const Poll = WriterImpl; + + pub fn __onClose(_: *This) void {} + pub fn __flush(_: *This) void {} + + pub fn refSelf(this: *This) *This { + this.ref(); + return this; + } + + pub fn init(fd: bun.FileDescriptor, evtloop: JSC.EventLoopHandle) *This { + const this = IOWriter.new(.{ + .fd = fd, + .evtloop = evtloop, + .concurrent_task = JSC.EventLoopTask.fromEventLoop(evtloop), + }); + + this.writer.parent = this; + if (comptime bun.Environment.isPosix) { + this.writer.handle = .{ + .poll = this.writer.createPoll(fd), + }; + } else { + this.writer.source = .{ + .file = bun.io.Source.openFile(fd), + }; + } + + print("IOWriter(0x{x}, fd={}) init noice", .{ @intFromPtr(this), fd }); + + return this; + } + + pub fn eventLoop(this: *This) JSC.EventLoopHandle { + return this.evtloop; + } + + /// Idempotent write call + pub fn write(this: *This) void { + if (bun.Environment.isWindows) { + log("IOWriter(0x{x}, fd={}) write() is_writing={any}", .{ @intFromPtr(this), this.fd, this.is_writing }); + if (this.is_writing) return; + this.is_writing = true; + if (this.writer.startWithCurrentPipe().asErr()) |e| { + _ = e; + @panic("TODO handle error"); + } + return; + } + + if (bun.Environment.allow_assert) { + if (this.writer.handle != .poll) @panic("Should be poll."); + } + if (!this.writer.handle.poll.isRegistered()) { + this.writer.write(); + } + } + + /// Cancel the chunks enqueued by the given writer by + /// marking them as dead + pub fn cancelChunks(this: *This, ptr_: anytype) void { + const ptr = switch (@TypeOf(ptr_)) { + ChildPtr => ptr_, + else => ChildPtr.init(ptr_), + }; + if (this.writers.len() == 0) return; + const idx = this.__idx; + const slice: []Writer = this.writers.sliceMutable(); + if (idx >= slice.len) return; + for (slice[idx..]) |*w| { + if (w.ptr.ptr.repr._ptr == ptr.ptr.repr._ptr) { + w.setDead(); + } + } + } + + const Writer = struct { + ptr: ChildPtr, + len: usize, + written: usize = 0, + bytelist: ?*bun.ByteList = null, + + pub fn rawPtr(this: Writer) ?*anyopaque { + return this.ptr.ptr.ptr(); + } + + pub fn isDead(this: Writer) bool { + return this.ptr.ptr.isNull(); + } + + pub fn setDead(this: *Writer) void { + this.ptr.ptr = ChildPtr.ChildPtrRaw.Null; + } + }; + + pub const Writers = SmolList(Writer, 2); + + /// Skips over dead children and increments `total_bytes_written` by the + /// amount they would have written so the buf is skipped as well + pub fn skipDead(this: *This) void { + const slice = this.writers.slice(); + for (slice[this.__idx..]) |*w| { + if (w.isDead()) { + this.__idx += 1; + this.total_bytes_written = w.len - w.written; + continue; + } + return; + } + return; + } + + pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void { + this.setWriting(false); + print("IOWriter(0x{x}, fd={}) write({d}, {})", .{ @intFromPtr(this), this.fd, amount, status }); + if (this.__idx >= this.writers.len()) return; + const child = this.writers.get(this.__idx); + if (child.isDead()) { + this.bump(child); + } else { + if (child.bytelist) |bl| { + const written_slice = this.buf.items[this.total_bytes_written .. this.total_bytes_written + amount]; + bl.append(bun.default_allocator, written_slice) catch bun.outOfMemory(); + } + this.total_bytes_written += amount; + child.written += amount; + if (status == .end_of_file) { + const not_fully_written = !this.isLastIdx(this.__idx) or child.written < child.len; + if (bun.Environment.allow_assert and not_fully_written) { + bun.Output.debugWarn("IOWriter(0x{x}) received done without fully writing data, check that onError is thrown", .{@intFromPtr(this)}); + } + return; + } + + if (child.written >= child.len) { + this.bump(child); + } + } + + const wrote_everything: bool = this.total_bytes_written >= this.buf.items.len; + + log("IOWriter(0x{x}, fd={}) wrote_everything={}, idx={d} writers={d}", .{ @intFromPtr(this), this.fd, wrote_everything, this.__idx, this.writers.len() }); + if (!wrote_everything and this.__idx < this.writers.len()) { + print("IOWriter(0x{x}, fd={}) poll again", .{ @intFromPtr(this), this.fd }); + if (comptime bun.Environment.isWindows) { + this.setWriting(true); + this.writer.write(); + } else this.writer.registerPoll(); + } + } + + pub fn onClose(this: *This) void { + this.setWriting(false); + } + + pub fn onError(this: *This, err__: bun.sys.Error) void { + this.setWriting(false); + this.err = err__.toSystemError(); + var seen_alloc = std.heap.stackFallback(@sizeOf(usize) * 64, bun.default_allocator); + var seen = std.ArrayList(usize).initCapacity(seen_alloc.get(), 64) catch bun.outOfMemory(); + defer seen.deinit(); + writer_loop: for (this.writers.slice()) |w| { + if (w.isDead()) continue; + const ptr = w.ptr.ptr.ptr(); + if (seen.items.len < 8) { + for (seen.items[0..]) |item| { + if (item == @intFromPtr(ptr)) { + continue :writer_loop; + } + } + } else if (std.mem.indexOfScalar(usize, seen.items[0..], @intFromPtr(ptr)) != null) { + continue :writer_loop; + } + + w.ptr.onWriteChunk(this.err); + seen.append(@intFromPtr(ptr)) catch bun.outOfMemory(); + } + } + + pub fn getBuffer(this: *This) []const u8 { + const writer = brk: { + const writer = this.writers.get(this.__idx); + if (!writer.isDead()) break :brk writer; + this.skipDead(); + if (this.__idx >= this.writers.len()) return ""; + break :brk this.writers.get(this.__idx); + }; + return this.buf.items[this.total_bytes_written .. this.total_bytes_written + writer.len]; + } + + pub fn bump(this: *This, current_writer: *Writer) void { + log("IOWriter(0x{x}) bump(0x{x} {s})", .{ @intFromPtr(this), @intFromPtr(current_writer), @tagName(current_writer.ptr.ptr.tag()) }); + const is_dead = current_writer.isDead(); + const child_ptr = current_writer.ptr; + + defer if (!is_dead) child_ptr.onWriteChunk(null); + + if (is_dead) { + this.skipDead(); + } else { + this.__idx += 1; + } + + if (this.__idx >= this.writers.len()) { + log("IOWriter(0x{x}) all writers complete: truncating", .{@intFromPtr(this)}); + this.buf.clearRetainingCapacity(); + this.__idx = 0; + this.writers.clearRetainingCapacity(); + this.total_bytes_written = 0; + return; + } + + if (this.total_bytes_written >= SHRINK_THRESHOLD) { + log("IOWriter(0x{x}) exceeded shrink threshold: truncating", .{@intFromPtr(this)}); + const replace_range_len = this.buf.items.len - this.total_bytes_written; + if (replace_range_len == 0) { + this.buf.clearRetainingCapacity(); + } else { + this.buf.replaceRange(bun.default_allocator, 0, replace_range_len, this.buf.items[this.total_bytes_written..replace_range_len]) catch bun.outOfMemory(); + this.buf.items.len = replace_range_len; + } + this.writers.truncate(this.__idx); + this.__idx = 0; + } + } + + pub fn enqueue(this: *This, ptr: anytype, bytelist: ?*bun.ByteList, buf: []const u8) void { + const childptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr); + if (buf.len == 0) { + log("IOWriter(0x{x}) enqueue EMPTY", .{@intFromPtr(this)}); + childptr.onWriteChunk(null); + return; + } + const writer: Writer = .{ + .ptr = childptr, + .len = buf.len, + .bytelist = bytelist, + }; + log("IOWriter(0x{x}) enqueue(0x{x} {s}, buf={s})", .{ @intFromPtr(this), @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), buf }); + this.buf.appendSlice(bun.default_allocator, buf) catch bun.outOfMemory(); + this.writers.append(writer); + this.write(); + } + + pub fn enqueueFmtBltn( + this: *This, + ptr: anytype, + bytelist: ?*bun.ByteList, + comptime kind: ?Interpreter.Builtin.Kind, + comptime fmt_: []const u8, + args: anytype, + ) void { + const cmd_str = comptime if (kind) |k| k.asString() ++ ": " else ""; + const fmt__ = cmd_str ++ fmt_; + this.enqueueFmt(ptr, bytelist, fmt__, args); + } - this.remain = this.remain[@min(bytes_written, this.remain.len)..]; - to_write = to_write[bytes_written..]; + pub fn enqueueFmt( + this: *This, + ptr: anytype, + bytelist: ?*bun.ByteList, + comptime fmt: []const u8, + args: anytype, + ) void { + var buf_writer = this.buf.writer(bun.default_allocator); + const start = this.buf.items.len; + buf_writer.print(fmt, args) catch bun.outOfMemory(); + const end = this.buf.items.len; + const writer: Writer = .{ + .ptr = if (@TypeOf(ptr) == ChildPtr) ptr else ChildPtr.init(ptr), + .len = end - start, + .bytelist = bytelist, + }; + log("IOWriter(0x{x}, fd={}) enqueue(0x{x} {s}, {s})", .{ @intFromPtr(this), this.fd, @intFromPtr(writer.rawPtr()), @tagName(writer.ptr.ptr.tag()), this.buf.items[start..end] }); + this.writers.append(writer); + this.write(); + } - // we are done or it accepts no more input - if (this.remain.len == 0 or (allow_blocking and bytes_written == 0)) { - this.deinit(); - return; - } - }, - } - } - } + pub fn asyncDeinit(this: *@This()) void { + print("IOWriter(0x{x}, fd={}) asyncDeinit", .{ @intFromPtr(this), this.fd }); + this.async_deinit.schedule(); + } - fn close(this: *BufferedWriter) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); - } + pub fn __deinit(this: *This) void { + print("IOWriter(0x{x}, fd={}) deinit", .{ @intFromPtr(this), this.fd }); + if (bun.Environment.allow_assert) std.debug.assert(this.ref_count == 0); + this.buf.deinit(bun.default_allocator); + if (this.fd != bun.invalid_fd) _ = bun.sys.close(this.fd); + this.destroy(); + } - if (this.fd != bun.invalid_fd) { - // _ = bun.sys.close(this.fd); - // this.fd = bun.invalid_fd; - } - } + pub fn isLastIdx(this: *This, idx: usize) bool { + return idx == this.writers.len() -| 1; + } - pub fn deinit(this: *BufferedWriter) void { - this.close(); - this.parent.onDone(this.err); + /// Only does things on windows + pub inline fn setWriting(this: *This, writing: bool) void { + if (bun.Environment.isWindows) { + log("IOWriter(0x{x}) setWriting({any})", .{ @intFromPtr(this), writing }); + this.is_writing = writing; } - }; + } }; -} +}; pub fn StatePtrUnion(comptime TypesValue: anytype) type { return struct { @@ -7630,9 +9248,8 @@ pub fn StatePtrUnion(comptime TypesValue: anytype) type { pub fn getChildPtrType(comptime Type: type) type { if (Type == Interpreter) return Interpreter.InterpreterChildPtr; - if (Type == InterpreterMini) return InterpreterMini.InterpreterChildPtr; if (!@hasDecl(Type, "ChildPtr")) { - @compileError(@typeName(Type) ++ " does not have ChildPtr"); + @compileError(@typeName(Type) ++ " does not have ChildPtr aksjdflkasjdflkasdjf"); } return Type.ChildPtr; } @@ -7783,7 +9400,6 @@ const CmdEnvIter = struct { /// allocated. pub fn ShellTask( comptime Ctx: type, - comptime EventLoopKind: JSC.EventLoopKind, /// Function to be called when the thread pool starts the task, this could /// be on anyone of the thread pool threads so be mindful of concurrency /// nuances @@ -7793,46 +9409,29 @@ pub fn ShellTask( comptime runFromMainThread_: fn (*Ctx) void, comptime print: fn (comptime fmt: []const u8, args: anytype) void, ) type { - const EventLoopRef = switch (EventLoopKind) { - .js => *JSC.EventLoop, - .mini => *JSC.MiniEventLoop, - }; - const event_loop_ref = struct { - fn get() EventLoopRef { - return switch (EventLoopKind) { - .js => JSC.VirtualMachine.get().event_loop, - .mini => bun.JSC.MiniEventLoop.global, - }; - } - }; - _ = event_loop_ref; // autofix - - const EventLoopTask = switch (EventLoopKind) { - .js => JSC.ConcurrentTask, - .mini => JSC.AnyTaskWithExtraContext, - }; return struct { task: WorkPoolTask = .{ .callback = &runFromThreadPool }, - event_loop: EventLoopRef, + event_loop: JSC.EventLoopHandle, // This is a poll because we want it to enter the uSockets loop ref: bun.Async.KeepAlive = .{}, - concurrent_task: EventLoopTask = .{}, + concurrent_task: JSC.EventLoopTask, pub const InnerShellTask = @This(); pub fn schedule(this: *@This()) void { print("schedule", .{}); - this.ref.ref(this.event_loop.getVmImpl()); + + this.ref.ref(this.event_loop); WorkPool.schedule(&this.task); } pub fn onFinish(this: *@This()) void { print("onFinish", .{}); const ctx = @fieldParentPtr(Ctx, "task", this); - if (comptime EventLoopKind == .js) { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(ctx, .manual_deinit)); + if (this.event_loop == .js) { + this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(ctx, .manual_deinit)); } else { - this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(ctx, "runFromMainThreadMini")); + this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(ctx, "runFromMainThreadMini")); } } @@ -7847,212 +9446,603 @@ pub fn ShellTask( pub fn runFromMainThread(this: *@This()) void { print("runFromJS", .{}); const ctx = @fieldParentPtr(Ctx, "task", this); - this.ref.unref(this.event_loop.getVmImpl()); + this.ref.unref(this.event_loop); runFromMainThread_(ctx); } }; } -const SliceBufferSrc = struct { - remain: []const u8 = "", +// pub const Builtin = + +inline fn errnocast(errno: anytype) u16 { + return @intCast(errno); +} + +inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) { + const Value = @typeInfo(@TypeOf(val)); + if (Value != .Int) @compileError("LHS of fastMod should be an int"); + if (Value.Int.signedness != .unsigned) @compileError("LHS of fastMod should be unsigned"); + if (!comptime std.math.isPowerOfTwo(rhs)) @compileError("RHS of fastMod should be power of 2"); + + return val & (rhs - 1); +} + +fn throwShellErr(e: *const bun.shell.ShellErr, event_loop: JSC.EventLoopHandle) void { + switch (event_loop) { + .mini => e.throwMini(), + .js => e.throwJS(event_loop.js.global), + } +} + +pub const ReadChunkAction = enum { + stop_listening, + cont, +}; + +pub const IOReaderChildPtr = struct { + ptr: ChildPtrRaw, + + pub const ChildPtrRaw = TaggedPointerUnion(.{ + Interpreter.Builtin.Cat, + }); + + pub fn init(p: anytype) IOReaderChildPtr { + return .{ + .ptr = ChildPtrRaw.init(p), + // .ptr = @ptrCast(p), + }; + } - fn bufToWrite(this: SliceBufferSrc, written: usize) []const u8 { - if (written >= this.remain.len) return ""; - return this.remain[written..]; + /// Return true if the child should be deleted + pub fn onReadChunk(this: IOReaderChildPtr, chunk: []const u8) ReadChunkAction { + return this.ptr.call("onIOReaderChunk", .{chunk}, ReadChunkAction); } - fn isDone(this: SliceBufferSrc, written: usize) bool { - return written >= this.remain.len; + pub fn onReaderDone(this: IOReaderChildPtr, err: ?JSC.SystemError) void { + return this.ptr.call("onIOReaderDone", .{err}, void); } }; -/// This is modified version of BufferedInput for file descriptors only. This -/// struct cleans itself up when it is done, so no need to call `.deinit()` on -/// it. -pub fn NewBufferedWriter(comptime Src: type, comptime Parent: type, comptime EventLoopKind: JSC.EventLoopKind) type { - const SrcHandler = struct { - src: Src, +pub const IOWriterChildPtr = struct { + ptr: ChildPtrRaw, + + pub const ChildPtrRaw = TaggedPointerUnion(.{ + Interpreter.Cmd, + Interpreter.Pipeline, + Interpreter.Builtin.Cd, + Interpreter.Builtin.Echo, + Interpreter.Builtin.Export, + Interpreter.Builtin.Ls, + Interpreter.Builtin.Ls.ShellLsOutputTask, + Interpreter.Builtin.Mv, + Interpreter.Builtin.Pwd, + Interpreter.Builtin.Rm, + Interpreter.Builtin.Which, + Interpreter.Builtin.Mkdir, + Interpreter.Builtin.Mkdir.ShellMkdirOutputTask, + Interpreter.Builtin.Touch, + Interpreter.Builtin.Touch.ShellTouchOutputTask, + Interpreter.Builtin.Cat, + }); + + pub fn init(p: anytype) IOWriterChildPtr { + return .{ + .ptr = ChildPtrRaw.init(p), + // .ptr = @ptrCast(p), + }; + } - inline fn bufToWrite(src: Src, written: usize) []const u8 { - if (!@hasDecl(Src, "bufToWrite")) @compileError("Need `bufToWrite`"); - return src.bufToWrite(written); - } + /// Called when the IOWriter writes a complete chunk of data the child enqueued + pub fn onWriteChunk(this: IOWriterChildPtr, err: ?JSC.SystemError) void { + return this.ptr.call("onIOWriterChunk", .{err}, void); + } +}; - inline fn isDone(src: Src, written: usize) bool { - if (!@hasDecl(Src, "isDone")) @compileError("Need `bufToWrite`"); - return src.isDone(written); +/// Shell modifications for syscalls, mostly to make windows work: +/// - Any function that returns a file descriptor will return a uv file descriptor +/// - Sometimes windows doesn't have `*at()` functions like `rmdirat` so we have to join the directory path with the target path +/// - Converts Posix absolute paths to Windows absolute paths on Windows +const ShellSyscall = struct { + fn getPath(dirfd: anytype, to: [:0]const u8, buf: *[bun.MAX_PATH_BYTES]u8) Maybe([:0]const u8) { + if (bun.Environment.isPosix) @compileError("Don't use this"); + if (bun.strings.eqlComptime(to[0..to.len], "/dev/null")) { + return .{ .result = shell.WINDOWS_DEV_NULL }; } - }; + if (ResolvePath.Platform.posix.isAbsolute(to[0..to.len])) { + const dirpath = brk: { + if (@TypeOf(dirfd) == bun.FileDescriptor) break :brk switch (Syscall.getFdPath(dirfd, buf)) { + .result => |path| path, + .err => |e| return .{ .err = e.withFd(dirfd) }, + }; + break :brk dirfd; + }; + const source_root = ResolvePath.windowsFilesystemRoot(dirpath); + std.mem.copyForwards(u8, buf[0..source_root.len], source_root); + @memcpy(buf[source_root.len..][0 .. to.len - 1], to[1..]); + buf[source_root.len + to.len - 1] = 0; + return .{ .result = buf[0 .. source_root.len + to.len - 1 :0] }; + } + if (ResolvePath.Platform.isAbsolute(.windows, to[0..to.len])) return .{ .result = to }; - return struct { - src: Src, - fd: bun.FileDescriptor, - poll_ref: ?*bun.Async.FilePoll = null, - written: usize = 0, - parent: Parent, - err: ?Syscall.Error = null, + const dirpath = brk: { + if (@TypeOf(dirfd) == bun.FileDescriptor) break :brk switch (Syscall.getFdPath(dirfd, buf)) { + .result => |path| path, + .err => |e| return .{ .err = e.withFd(dirfd) }, + }; + @memcpy(buf[0..dirfd.len], dirfd[0..dirfd.len]); + break :brk buf[0..dirfd.len]; + }; + + const parts: []const []const u8 = &.{ + dirpath[0..dirpath.len], + to[0..to.len], + }; + const joined = ResolvePath.joinZBuf(buf, parts, .auto); + return .{ .result = joined }; + } - pub const ParentType = Parent; + fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = switch (getPath(dir, path_, &buf)) { + .err => |e| return .{ .err = e }, + .result => |p| p, + }; + + return switch (Syscall.stat(path)) { + .err => |e| .{ .err = e.clone(bun.default_allocator) catch bun.outOfMemory() }, + .result => |s| .{ .result = s }, + }; + } - const print = bun.Output.scoped(.BufferedWriter, false); + fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { + if (bun.Environment.isWindows) { + if (flags & os.O.DIRECTORY != 0) { + if (ResolvePath.Platform.posix.isAbsolute(path[0..path.len])) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const p = switch (getPath(dir, path, &buf)) { + .result => |p| p, + .err => |e| return .{ .err = e }, + }; + return switch (Syscall.openDirAtWindowsA(dir, p, true, flags & os.O.NOFOLLOW != 0)) { + .result => |fd| .{ .result = bun.toLibUVOwnedFD(fd) }, + .err => |e| return .{ .err = e.withPath(path) }, + }; + } + return switch (Syscall.openDirAtWindowsA(dir, path, true, flags & os.O.NOFOLLOW != 0)) { + .result => |fd| .{ .result = bun.toLibUVOwnedFD(fd) }, + .err => |e| return .{ .err = e.withPath(path) }, + }; + } - pub fn isDone(this: *@This()) bool { - return SrcHandler.isDone(this.src, this.written) or this.err != null; + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const p = switch (getPath(dir, path, &buf)) { + .result => |p| p, + .err => |e| return .{ .err = e }, + }; + return bun.sys.open(p, flags, perm); } - pub const event_loop_kind = EventLoopKind; - pub usingnamespace JSC.WebCore.NewReadyWatcher(@This(), .writable, onReady); + const fd = switch (Syscall.openat(dir, path, flags, perm)) { + .result => |fd| fd, + .err => |e| return .{ .err = e.withPath(path) }, + }; + if (bun.Environment.isWindows) { + return .{ .result = bun.toLibUVOwnedFD(fd) }; + } + return .{ .result = fd }; + } - pub fn onReady(this: *@This(), _: i64) void { - if (this.fd == bun.invalid_fd) { - return; - } + pub fn open(file_path: [:0]const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { + const fd = switch (Syscall.open(file_path, flags, perm)) { + .result => |fd| fd, + .err => |e| return .{ .err = e }, + }; + if (bun.Environment.isWindows) { + return .{ .result = bun.toLibUVOwnedFD(fd) }; + } + return .{ .result = fd }; + } - this.__write(); + pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { + if (bun.Environment.isWindows) { + return switch (Syscall.dup(fd)) { + .result => |f| return .{ .result = bun.toLibUVOwnedFD(f) }, + .err => |e| return .{ .err = e }, + }; } + return Syscall.dup(fd); + } - pub fn writeIfPossible(this: *@This(), comptime is_sync: bool) void { - if (SrcHandler.bufToWrite(this.src, 0).len == 0) return this.deinit(); - if (comptime !is_sync) { - // we ask, "Is it possible to write right now?" - // we do this rather than epoll or kqueue() - // because we don't want to block the thread waiting for the write - switch (bun.isWritable(this.fd)) { - .ready => { - if (this.poll_ref) |poll| { - poll.flags.insert(.writable); - poll.flags.insert(.fifo); - std.debug.assert(poll.flags.contains(.poll_writable)); - } - }, - .hup => { - this.deinit(); - return; - }, - .not_ready => { - if (!this.isWatching()) this.watch(this.fd); - return; - }, + pub fn unlinkatWithFlags(dirfd: anytype, to: [:0]const u8, flags: c_uint) Maybe(void) { + if (bun.Environment.isWindows) { + if (flags & std.os.AT.REMOVEDIR != 0) return ShellSyscall.rmdirat(dirfd, to); + + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path = brk: { + switch (ShellSyscall.getPath(dirfd, to, &buf)) { + .err => |e| return .{ .err = e }, + .result => |p| break :brk p, } - } + }; - this.writeAllowBlocking(is_sync); + return switch (Syscall.unlink(path)) { + .result => return Maybe(void).success, + .err => |e| { + log("unlinkatWithFlags({s}) = {s}", .{ path, @tagName(e.getErrno()) }); + return .{ .err = e.withPath(bun.default_allocator.dupe(u8, path) catch bun.outOfMemory()) }; + }, + }; } + if (@TypeOf(dirfd) != bun.FileDescriptor) { + @compileError("Bad type: " ++ @typeName(@TypeOf(dirfd))); + } + return Syscall.unlinkatWithFlags(dirfd, to, flags); + } - /// Calling this directly will block if the fd is not opened with non - /// blocking option. If the fd is blocking, you should call - /// `writeIfPossible()` first, which will check if the fd is writable. If so - /// it will then call this function, if not, then it will poll for the fd to - /// be writable - pub fn __write(this: *@This()) void { - this.writeAllowBlocking(false); + pub fn rmdirat(dirfd: anytype, to: [:0]const u8) Maybe(void) { + if (bun.Environment.isWindows) { + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const path: []const u8 = brk: { + switch (getPath(dirfd, to, &buf)) { + .result => |p| break :brk p, + .err => |e| return .{ .err = e }, + } + }; + var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; + const wpath = bun.strings.toWPath(&wide_buf, path); + while (true) { + if (windows.RemoveDirectoryW(wpath) == 0) { + const errno = Syscall.getErrno(420); + if (errno == .INTR) continue; + log("rmdirat({s}) = {d}: {s}", .{ path, @intFromEnum(errno), @tagName(errno) }); + return .{ .err = Syscall.Error.fromCode(errno, .rmdir) }; + } + log("rmdirat({s}) = {d}", .{ path, 0 }); + return Maybe(void).success; + } } - pub fn writeAllowBlocking(this: *@This(), allow_blocking: bool) void { - _ = allow_blocking; // autofix + return Syscall.rmdirat(dirfd, to); + } +}; + +/// A task that can write to stdout and/or stderr +pub fn OutputTask( + comptime Parent: type, + comptime vtable: struct { + writeErr: *const fn (*Parent, childptr: anytype, []const u8) CoroutineResult, + onWriteErr: *const fn (*Parent) void, + writeOut: *const fn (*Parent, childptr: anytype, *OutputSrc) CoroutineResult, + onWriteOut: *const fn (*Parent) void, + onDone: *const fn (*Parent) void, + }, +) type { + return struct { + parent: *Parent, + output: OutputSrc, + state: enum { + waiting_write_err, + waiting_write_out, + done, + }, - var to_write = SrcHandler.bufToWrite(this.src, this.written); + pub fn deinit(this: *@This()) void { + if (comptime bun.Environment.allow_assert) std.debug.assert(this.state == .done); + vtable.onDone(this.parent); + this.output.deinit(); + bun.destroy(this); + } - if (to_write.len == 0) { - // we are done! - // this.closeFDIfOpen(); - if (SrcHandler.isDone(this.src, this.written)) { - this.deinit(); + pub fn start(this: *@This(), errbuf: ?[]const u8) void { + this.state = .waiting_write_err; + if (errbuf) |err| { + switch (vtable.writeErr(this.parent, this, err)) { + .cont => { + this.next(); + }, + .yield => return, } return; } - - if (comptime bun.Environment.allow_assert) { - // bun.assertNonBlocking(this.fd); + this.state = .waiting_write_out; + switch (vtable.writeOut(this.parent, this, &this.output)) { + .cont => { + vtable.onWriteOut(this.parent); + this.state = .done; + this.deinit(); + }, + .yield => return, } + } - while (to_write.len > 0) { - switch (bun.sys.write(this.fd, to_write)) { - .err => |e| { - if (e.isRetry()) { - log("write({d}) retry", .{ - to_write.len, - }); + pub fn next(this: *@This()) void { + switch (this.state) { + .waiting_write_err => { + vtable.onWriteErr(this.parent); + this.state = .waiting_write_out; + switch (vtable.writeOut(this.parent, this, &this.output)) { + .cont => { + vtable.onWriteOut(this.parent); + this.state = .done; + this.deinit(); + }, + .yield => return, + } + }, + .waiting_write_out => { + vtable.onWriteOut(this.parent); + this.state = .done; + this.deinit(); + }, + .done => @panic("Invalid state"), + } + } - this.watch(this.fd); - this.poll_ref.?.flags.insert(.fifo); - return; - } + pub fn onIOWriterChunk(this: *@This(), err: ?JSC.SystemError) void { + if (err) |e| { + e.deref(); + } - if (e.getErrno() == .PIPE) { + switch (this.state) { + .waiting_write_err => { + vtable.onWriteErr(this.parent); + this.state = .waiting_write_out; + switch (vtable.writeOut(this.parent, this, &this.output)) { + .cont => { + vtable.onWriteOut(this.parent); + this.state = .done; this.deinit(); - return; - } - - // fail - log("write({d}) fail: {d}", .{ to_write.len, e.errno }); - this.err = e; - this.deinit(); - return; - }, + }, + .yield => return, + } + }, + .waiting_write_out => { + vtable.onWriteOut(this.parent); + this.state = .done; + this.deinit(); + }, + .done => @panic("Invalid state"), + } + } + }; +} - .result => |bytes_written| { - this.written += bytes_written; +/// All owned memory is assumed to be allocated with `bun.default_allocator` +pub const OutputSrc = union(enum) { + arrlist: std.ArrayListUnmanaged(u8), + owned_buf: []const u8, + borrowed_buf: []const u8, + + pub fn slice(this: *OutputSrc) []const u8 { + return switch (this.*) { + .arrlist => this.arrlist.items[0..], + .owned_buf => this.owned_buf, + .borrowed_buf => this.borrowed_buf, + }; + } - log( - "write({d}) {d}", - .{ - to_write.len, - bytes_written, - }, - ); + pub fn deinit(this: *OutputSrc) void { + switch (this.*) { + .arrlist => { + this.arrlist.deinit(bun.default_allocator); + }, + .owned_buf => { + bun.default_allocator.free(this.owned_buf); + }, + .borrowed_buf => {}, + } + } +}; - // this.remain = this.remain[@min(bytes_written, this.remain.len)..]; - // to_write = to_write[bytes_written..]; +/// Custom parse error for invalid options +pub const ParseError = union(enum) { + illegal_option: []const u8, + unsupported: []const u8, + show_usage, +}; +pub fn unsupportedFlag(comptime name: []const u8) []const u8 { + return "unsupported option, please open a GitHub issue -- " ++ name ++ "\n"; +} +pub const ParseFlagResult = union(enum) { continue_parsing, done, illegal_option: []const u8, unsupported: []const u8, show_usage }; +pub fn FlagParser(comptime Opts: type) type { + return struct { + pub const Result = @import("../result.zig").Result; - // // we are done or it accepts no more input - // if (this.remain.len == 0 or (allow_blocking and bytes_written == 0)) { - // this.deinit(); - // return; - // } + pub fn parseFlags(opts: Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) { + var idx: usize = 0; + if (args.len == 0) { + return .{ .ok = null }; + } - to_write = SrcHandler.bufToWrite(this.src, this.written); - if (to_write.len == 0) { - if (SrcHandler.isDone(this.src, this.written)) { - this.deinit(); - return; - } - } + while (idx < args.len) : (idx += 1) { + const flag = args[idx]; + switch (parseFlag(opts, flag[0..std.mem.len(flag)])) { + .done => { + const filepath_args = args[idx..]; + return .{ .ok = filepath_args }; }, + .continue_parsing => {}, + .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, + .unsupported => |unsp| return .{ .err = .{ .unsupported = unsp } }, + .show_usage => return .{ .err = .show_usage }, } } + + return .{ .err = .show_usage }; } - fn close(this: *@This()) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); + fn parseFlag(opts: Opts, flag: []const u8) ParseFlagResult { + if (flag.len == 0) return .done; + if (flag[0] != '-') return .done; + + if (flag.len == 1) return .{ .illegal_option = "-" }; + + if (flag.len > 2 and flag[1] == '-') { + if (opts.parseLong(flag)) |result| return result; } - if (this.fd != bun.invalid_fd) { - // _ = bun.sys.close(this.fd); - // this.fd = bun.invalid_fd; + const small_flags = flag[1..]; + for (small_flags, 0..) |char, i| { + if (opts.parseShort(char, small_flags, i)) |err| { + return err; + } } - } - pub fn deinit(this: *@This()) void { - this.close(); - this.parent.onDone(this.err); + return .continue_parsing; } }; } -// pub const Builtin = +/// A list that can store its items inlined, and promote itself to a heap allocated bun.ByteList +pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type { + return union(enum) { + inlined: Inlined, + heap: ByteList, -inline fn errnocast(errno: anytype) u16 { - return @intCast(errno); -} + const ByteList = bun.BabyList(T); -inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) { - const Value = @typeInfo(@TypeOf(val)); - if (Value != .Int) @compileError("LHS of fastMod should be an int"); - if (Value.Int.signedness != .unsigned) @compileError("LHS of fastMod should be unsigned"); - if (!comptime std.math.isPowerOfTwo(rhs)) @compileError("RHS of fastMod should be power of 2"); + pub const Inlined = struct { + items: [INLINED_MAX]T = undefined, + len: u32 = 0, - return val & (rhs - 1); + pub fn promote(this: *Inlined, n: usize, new: T) bun.BabyList(T) { + var list = bun.BabyList(T).initCapacity(bun.default_allocator, n) catch bun.outOfMemory(); + list.append(bun.default_allocator, this.items[0..INLINED_MAX]) catch bun.outOfMemory(); + list.push(bun.default_allocator, new) catch bun.outOfMemory(); + return list; + } + + pub fn orderedRemove(this: *Inlined, idx: usize) T { + if (this.len - 1 == idx) return this.pop(); + const slice_to_shift = this.items[idx + 1 .. this.len]; + std.mem.copyForwards(T, this.items[idx .. this.len - 1], slice_to_shift); + this.len -= 1; + } + + pub fn swapRemove(this: *Inlined, idx: usize) T { + if (this.len - 1 == idx) return this.pop(); + + const old_item = this.items[idx]; + this.items[idx] = this.pop(); + return old_item; + } + + pub fn pop(this: *Inlined) T { + const ret = this.items[this.items.len - 1]; + this.len -= 1; + return ret; + } + }; + + pub inline fn len(this: *@This()) usize { + return switch (this.*) { + .inlined => this.inlined.len, + .heap => this.heap.len, + }; + } + + pub fn orderedRemove(this: *@This(), idx: usize) void { + switch (this.*) { + .heap => { + var list = this.heap.listManaged(bun.default_allocator); + _ = list.orderedRemove(idx); + }, + .inlined => { + _ = this.inlined.orderedRemove(idx); + }, + } + } + + pub fn swapRemove(this: *@This(), idx: usize) void { + switch (this.*) { + .heap => { + var list = this.heap.listManaged(bun.default_allocator); + _ = list.swapRemove(idx); + }, + .inlined => { + _ = this.inlined.swapRemove(idx); + }, + } + } + + pub fn truncate(this: *@This(), starting_idx: usize) void { + switch (this.*) { + .inlined => { + if (starting_idx >= this.inlined.len) return; + const slice_to_move = this.inlined.items[starting_idx..this.inlined.len]; + std.mem.copyForwards(T, this.inlined.items[0..starting_idx], slice_to_move); + }, + .heap => { + const new_len = this.heap.len - starting_idx; + this.heap.replaceRange(0, starting_idx, this.heap.ptr[starting_idx..this.heap.len]) catch bun.outOfMemory(); + this.heap.len = @intCast(new_len); + }, + } + } + + pub inline fn sliceMutable(this: *@This()) []T { + return switch (this.*) { + .inlined => { + if (this.inlined.len == 0) return &[_]T{}; + return this.inlined.items[0..this.inlined.len]; + }, + .heap => { + if (this.heap.len == 0) return &[_]T{}; + return this.heap.slice(); + }, + }; + } + + pub inline fn slice(this: *@This()) []const T { + return switch (this.*) { + .inlined => { + if (this.inlined.len == 0) return &[_]T{}; + return this.inlined.items[0..this.inlined.len]; + }, + .heap => { + if (this.heap.len == 0) return &[_]T{}; + return this.heap.slice(); + }, + }; + } + + pub inline fn get(this: *@This(), idx: usize) *T { + return switch (this.*) { + .inlined => { + if (bun.Environment.allow_assert) { + if (idx >= this.inlined.len) @panic("Index out of bounds"); + } + return &this.inlined.items[idx]; + }, + .heap => &this.heap.ptr[idx], + }; + } + + pub fn append(this: *@This(), new: T) void { + switch (this.*) { + .inlined => { + if (this.inlined.len == INLINED_MAX) { + this.* = .{ .heap = this.inlined.promote(INLINED_MAX, new) }; + return; + } + this.inlined.items[this.inlined.len] = new; + this.inlined.len += 1; + }, + .heap => { + this.heap.push(bun.default_allocator, new) catch bun.outOfMemory(); + }, + } + } + + pub fn clearRetainingCapacity(this: *@This()) void { + switch (this.*) { + .inlined => { + this.inlined.len = 0; + }, + .heap => { + this.heap.clearRetainingCapacity(); + }, + } + } + }; } diff --git a/src/shell/shell.zig b/src/shell/shell.zig index 5873c4dce422da..45db900411959d 100644 --- a/src/shell/shell.zig +++ b/src/shell/shell.zig @@ -25,14 +25,22 @@ pub const subproc = @import("./subproc.zig"); pub const EnvMap = interpret.EnvMap; pub const EnvStr = interpret.EnvStr; pub const Interpreter = eval.Interpreter; -pub const InterpreterMini = eval.InterpreterMini; pub const Subprocess = subproc.ShellSubprocess; -pub const SubprocessMini = subproc.ShellSubprocessMini; +// pub const IOWriter = interpret.IOWriter; +// pub const SubprocessMini = subproc.ShellSubprocessMini; const GlobWalker = Glob.GlobWalker_(null, true); // const GlobWalker = Glob.BunGlobWalker; -pub const SUBSHELL_TODO_ERROR = "Subshells are not implemented, please open GitHub issue."; +pub const SUBSHELL_TODO_ERROR = "Subshells are not implemented, please open GitHub issue!"; + +/// Using these instead of `bun.STD{IN,OUT,ERR}_FD` to makesure we use uv fd +pub const STDIN_FD: bun.FileDescriptor = if (bun.Environment.isWindows) bun.FDImpl.fromUV(0).encode() else bun.STDIN_FD; +pub const STDOUT_FD: bun.FileDescriptor = if (bun.Environment.isWindows) bun.FDImpl.fromUV(1).encode() else bun.STDOUT_FD; +pub const STDERR_FD: bun.FileDescriptor = if (bun.Environment.isWindows) bun.FDImpl.fromUV(2).encode() else bun.STDERR_FD; + +pub const POSIX_DEV_NULL: [:0]const u8 = "/dev/null"; +pub const WINDOWS_DEV_NULL: [:0]const u8 = "NUL"; /// The strings in this type are allocated with event loop ctx allocator pub const ShellErr = union(enum) { @@ -41,9 +49,13 @@ pub const ShellErr = union(enum) { invalid_arguments: struct { val: []const u8 = "" }, todo: []const u8, - pub fn newSys(e: Syscall.Error) @This() { + pub fn newSys(e: anytype) @This() { return .{ - .sys = e.toSystemError(), + .sys = switch (@TypeOf(e)) { + Syscall.Error => e.toSystemError(), + JSC.SystemError => e, + else => @compileError("Invalid `e`: " ++ @typeName(e)), + }, }; } @@ -68,8 +80,9 @@ pub const ShellErr = union(enum) { } } - pub fn throwJS(this: @This(), globalThis: *JSC.JSGlobalObject) void { - switch (this) { + pub fn throwJS(this: *const @This(), globalThis: *JSC.JSGlobalObject) void { + defer this.deinit(bun.default_allocator); + switch (this.*) { .sys => { const err = this.sys.toErrorInstance(globalThis); globalThis.throwValue(err); @@ -78,7 +91,7 @@ pub const ShellErr = union(enum) { var str = JSC.ZigString.init(this.custom); str.markUTF8(); const err_value = str.toErrorInstance(globalThis); - globalThis.vm().throwError(globalThis, err_value); + globalThis.throwValue(err_value); // this.bunVM().allocator.free(JSC.ZigString.untagged(str._unsafe_ptr_do_not_use)[0..str.len]); }, .invalid_arguments => { @@ -91,6 +104,7 @@ pub const ShellErr = union(enum) { } pub fn throwMini(this: @This()) void { + defer this.deinit(bun.default_allocator); switch (this) { .sys => { const err = this.sys; diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index af7673bc031d36..d910d9332a4347 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -17,1905 +17,1383 @@ const Async = bun.Async; // const IPC = @import("../bun.js/ipc.zig"); const uws = bun.uws; -const PosixSpawn = @import("../bun.js/api/bun/spawn.zig").PosixSpawn; +const PosixSpawn = bun.spawn; const util = @import("./util.zig"); pub const Stdio = util.Stdio; - +const FileSink = JSC.WebCore.FileSink; // pub const ShellSubprocess = NewShellSubprocess(.js); // pub const ShellSubprocessMini = NewShellSubprocess(.mini); -pub const ShellSubprocess = NewShellSubprocess(.js, bun.shell.interpret.Interpreter.Cmd); -pub const ShellSubprocessMini = NewShellSubprocess(.mini, bun.shell.interpret.InterpreterMini.Cmd); +const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor; -pub fn NewShellSubprocess(comptime EventLoopKind: JSC.EventLoopKind, comptime ShellCmd: type) type { - const EventLoopRef = switch (EventLoopKind) { - .js => *JSC.EventLoop, - .mini => *JSC.MiniEventLoop, - }; - _ = EventLoopRef; // autofix +const BufferedInput = struct {}; - const GlobalRef = switch (EventLoopKind) { - .js => *JSC.JSGlobalObject, - .mini => *JSC.MiniEventLoop, - }; +/// TODO Set this to interpreter +const ShellCmd = bun.shell.Interpreter.Cmd; - const FIFO = switch (EventLoopKind) { - .js => JSC.WebCore.FIFO, - .mini => JSC.WebCore.FIFOMini, - }; - const FileSink = switch (EventLoopKind) { - .js => JSC.WebCore.FileSink, - .mini => JSC.WebCore.FileSinkMini, - }; +const log = Output.scoped(.SHELL_SUBPROC, false); - const Vm = switch (EventLoopKind) { - .js => *JSC.VirtualMachine, - .mini => *JSC.MiniEventLoop, - }; +pub const ShellSubprocess = struct { + const Subprocess = @This(); - const get_vm = struct { - fn get() Vm { - return switch (EventLoopKind) { - .js => JSC.VirtualMachine.get(), - .mini => bun.JSC.MiniEventLoop.global, - }; - } - }; + pub const default_max_buffer_size = 1024 * 1024 * 4; + pub const Process = bun.spawn.Process; - // const ShellCmd = switch (EventLoopKind) { - // .js => bun.shell.interpret.Interpreter.Cmd, - // .mini => bun.shell.interpret.InterpreterMini.Cmd, - // }; - // const ShellCmd = bun.shell.interpret.NewInterpreter(EventLoopKind); + cmd_parent: ?*ShellCmd = null, - return struct { - const Subprocess = @This(); - const log = Output.scoped(.SHELL_SUBPROC, false); - pub const default_max_buffer_size = 1024 * 1024 * 4; + process: *Process, - pub const GlobalHandle = switch (EventLoopKind) { - .js => bun.shell.GlobalJS, - .mini => bun.shell.GlobalMini, - }; + stdin: Writable = undefined, + stdout: Readable = undefined, + stderr: Readable = undefined, - cmd_parent: ?*ShellCmd = null, - pid: std.os.pid_t, - // on macOS, this is nothing - // on linux, it's a pidfd - pidfd: if (Environment.isLinux) bun.FileDescriptor else u0 = if (Environment.isLinux) bun.invalid_fd else 0, - - stdin: Writable, - stdout: Readable, - stderr: Readable, - poll: Poll = Poll{ .poll_ref = null }, - - // on_exit_callback: JSC.Strong = .{}, - - exit_code: ?u8 = null, - signal_code: ?SignalCode = null, - waitpid_err: ?bun.sys.Error = null, - - globalThis: GlobalRef, - // observable_getters: std.enums.EnumSet(enum { - // stdin, - // stdout, - // stderr, - // }) = .{}, - closed: std.enums.EnumSet(enum { - stdin, - stdout, - stderr, - }) = .{}, - this_jsvalue: JSC.JSValue = .zero, + event_loop: JSC.EventLoopHandle, - // ipc_mode: IPCMode, - // ipc_callback: JSC.Strong = .{}, - // ipc: IPC.IPCData, - flags: Flags = .{}, - - // pub const IPCMode = enum { - // none, - // bun, - // // json, - // }; - - pub const OutKind = util.OutKind; - // pub const Stdio = util.Stdio; - - pub const Flags = packed struct(u3) { - is_sync: bool = false, - killed: bool = false, - waiting_for_onexit: bool = false, - }; - pub const SignalCode = bun.SignalCode; - - pub const Poll = union(enum) { - poll_ref: ?*Async.FilePoll, - wait_thread: WaitThreadPoll, - }; + closed: std.enums.EnumSet(enum { + stdin, + stdout, + stderr, + }) = .{}, + this_jsvalue: JSC.JSValue = .zero, - pub const WaitThreadPoll = struct { - ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - poll_ref: Async.KeepAlive = .{}, - }; + flags: Flags = .{}, - pub const Writable = union(enum) { - pipe: *FileSink, - pipe_to_readable_stream: struct { - pipe: *FileSink, - readable_stream: JSC.WebCore.ReadableStream, - }, - fd: bun.FileDescriptor, - buffered_input: BufferedInput, - inherit: void, - ignore: void, + pub const OutKind = util.OutKind; - pub fn ref(this: *Writable) void { - switch (this.*) { - .pipe => { - if (this.pipe.poll_ref) |poll| { - poll.enableKeepingProcessAlive(get_vm.get()); - } - }, - else => {}, - } - } - - pub fn unref(this: *Writable) void { - switch (this.*) { - .pipe => { - if (this.pipe.poll_ref) |poll| { - poll.enableKeepingProcessAlive(get_vm.get()); - } - }, - else => {}, - } - } - - // When the stream has closed we need to be notified to prevent a use-after-free - // We can test for this use-after-free by enabling hot module reloading on a file and then saving it twice - pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { - this.* = .{ - .ignore = {}, - }; - } - pub fn onReady(_: *Writable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} - pub fn onStart(_: *Writable) void {} - - pub fn init(subproc: *Subprocess, stdio: Stdio, fd: bun.FileDescriptor, globalThis: GlobalRef) !Writable { - switch (stdio) { - .pipe => { - // var sink = try globalThis.bunVM().allocator.create(JSC.WebCore.FileSink); - var sink = try GlobalHandle.init(globalThis).allocator().create(FileSink); - sink.* = .{ - .fd = fd, - .buffer = bun.ByteList{}, - .allocator = GlobalHandle.init(globalThis).allocator(), - .auto_close = true, - }; - sink.mode = std.os.S.IFIFO; - sink.watch(fd); - if (stdio == .pipe) { - if (stdio.pipe) |readable| { - if (comptime EventLoopKind == .mini) @panic("FIXME TODO error gracefully but wait can this even happen"); - return Writable{ - .pipe_to_readable_stream = .{ - .pipe = sink, - .readable_stream = readable, - }, - }; - } - } - - return Writable{ .pipe = sink }; - }, - .dup2, .array_buffer, .blob => { - var buffered_input: BufferedInput = .{ .fd = fd, .source = undefined, .subproc = subproc }; - switch (stdio) { - .array_buffer => |array_buffer| { - buffered_input.source = .{ .array_buffer = array_buffer.buf }; - }, - .blob => |blob| { - buffered_input.source = .{ .blob = blob }; - }, - else => unreachable, - } - return Writable{ .buffered_input = buffered_input }; - }, - .fd => { - return Writable{ .fd = fd }; - }, - .inherit => { - return Writable{ .inherit = {} }; - }, - .path, .ignore => { - return Writable{ .ignore = {} }; - }, - } - } + pub fn onStaticPipeWriterDone(this: *ShellSubprocess) void { + log("Subproc(0x{x}) onStaticPipeWriterDone(cmd=0x{x}))", .{ @intFromPtr(this), if (this.cmd_parent) |cmd| @intFromPtr(cmd) else 0 }); + if (this.cmd_parent) |cmd| { + cmd.bufferedInputClose(); + } + } + + const Writable = union(enum) { + pipe: *JSC.WebCore.FileSink, + fd: bun.FileDescriptor, + buffer: *StaticPipeWriter, + memfd: bun.FileDescriptor, + inherit: void, + ignore: void, + + pub fn hasPendingActivity(this: *const Writable) bool { + return switch (this.*) { + // we mark them as .ignore when they are closed, so this must be true + .pipe => true, + .buffer => true, + else => false, + }; + } - pub fn toJS(this: Writable, globalThis: *JSC.JSGlobalObject) JSValue { - return switch (this) { - .pipe => |pipe| pipe.toJS(globalThis), - .fd => |fd| JSValue.jsNumber(fd), - .ignore => JSValue.jsUndefined(), - .inherit => JSValue.jsUndefined(), - .buffered_input => JSValue.jsUndefined(), - .pipe_to_readable_stream => this.pipe_to_readable_stream.readable_stream.value, - }; + pub fn ref(this: *Writable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(true); + }, + .buffer => { + this.buffer.updateRef(true); + }, + else => {}, } + } - pub fn finalize(this: *Writable) void { - return switch (this.*) { - .pipe => |pipe| { - pipe.close(); - }, - .pipe_to_readable_stream => |*pipe_to_readable_stream| { - _ = pipe_to_readable_stream.pipe.end(null); - }, - .fd => |fd| { - _ = bun.sys.close(fd); - this.* = .{ .ignore = {} }; - }, - .buffered_input => { - this.buffered_input.deinit(); - }, - .ignore => {}, - .inherit => {}, - }; + pub fn unref(this: *Writable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(false); + }, + .buffer => { + this.buffer.updateRef(false); + }, + else => {}, } + } - pub fn close(this: *Writable) void { - return switch (this.*) { - .pipe => {}, - .pipe_to_readable_stream => |*pipe_to_readable_stream| { - _ = pipe_to_readable_stream.pipe.end(null); - }, - .fd => |fd| { - _ = bun.sys.close(fd); - this.* = .{ .ignore = {} }; - }, - .buffered_input => { - this.buffered_input.deinit(); - }, - .ignore => {}, - .inherit => {}, - }; + // When the stream has closed we need to be notified to prevent a use-after-free + // We can test for this use-after-free by enabling hot module reloading on a file and then saving it twice + pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { + switch (this.*) { + .buffer => { + this.buffer.deref(); + }, + .pipe => { + this.pipe.deref(); + }, + else => {}, } - }; - - pub const Readable = union(enum) { - fd: bun.FileDescriptor, + this.* = .{ + .ignore = {}, + }; + } + pub fn onReady(_: *Writable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} + pub fn onStart(_: *Writable) void {} - pipe: Pipe, - inherit: void, - ignore: void, - closed: void, + pub fn init( + stdio: Stdio, + event_loop: JSC.EventLoopHandle, + subprocess: *Subprocess, + result: StdioResult, + ) !Writable { + assertStdioResult(result); - pub fn ref(this: *Readable) void { - switch (this.*) { + if (Environment.isWindows) { + switch (stdio) { .pipe => { - if (this.pipe == .buffer) { - if (this.pipe.buffer.fifo.poll_ref) |poll| { - poll.enableKeepingProcessAlive(get_vm.get()); - } - } - }, - else => {}, - } - } + if (result == .buffer) { + const pipe = JSC.WebCore.FileSink.createWithPipe(event_loop, result.buffer); - pub fn unref(this: *Readable) void { - switch (this.*) { - .pipe => { - if (this.pipe == .buffer) { - if (this.pipe.buffer.fifo.poll_ref) |poll| { - poll.enableKeepingProcessAlive(get_vm.get()); + switch (pipe.writer.startWithCurrentPipe()) { + .result => {}, + .err => |err| { + _ = err; // autofix + pipe.deref(); + return error.UnexpectedCreatingStdin; + }, } - } - }, - else => {}, - } - } - - pub const Pipe = union(enum) { - stream: JSC.WebCore.ReadableStream, - buffer: BufferedOutput, - - pub fn finish(this: *@This()) void { - if (this.* == .stream and this.stream.ptr == .File) { - this.stream.ptr.File.finish(); - } - } - - pub fn done(this: *@This()) void { - if (this.* == .stream) { - if (this.stream.ptr == .File) this.stream.ptr.File.setSignal(JSC.WebCore.Signal{}); - this.stream.done(); - return; - } - - this.buffer.close(); - } - - pub fn toJS(this: *@This(), readable: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { - if (this.* != .stream) { - const stream = this.buffer.toReadableStream(globalThis, exited); - this.* = .{ .stream = stream }; - } - - if (this.stream.ptr == .File) { - this.stream.ptr.File.setSignal(JSC.WebCore.Signal.init(readable)); - } - return this.stream.toJS(); - } - }; + // TODO: uncoment this when is ready, commented because was not compiling + // subprocess.weak_file_sink_stdin_ptr = pipe; + // subprocess.flags.has_stdin_destructor_called = false; - pub fn init(subproc: *Subprocess, comptime kind: OutKind, stdio: Stdio, fd: bun.FileDescriptor, allocator: std.mem.Allocator, max_size: u32) Readable { - return switch (stdio) { - .ignore => Readable{ .ignore = {} }, - .pipe => { - var subproc_readable_ptr = subproc.getIO(kind); - subproc_readable_ptr.* = Readable{ .pipe = .{ .buffer = undefined } }; - BufferedOutput.initWithAllocator(subproc, &subproc_readable_ptr.pipe.buffer, kind, allocator, fd, max_size); - return subproc_readable_ptr.*; - }, - .inherit => { - // Same as pipe - if (stdio.inherit.captured != null) { - var subproc_readable_ptr = subproc.getIO(kind); - subproc_readable_ptr.* = Readable{ .pipe = .{ .buffer = undefined } }; - BufferedOutput.initWithAllocator(subproc, &subproc_readable_ptr.pipe.buffer, kind, allocator, fd, max_size); - subproc_readable_ptr.pipe.buffer.out = stdio.inherit.captured.?; - subproc_readable_ptr.pipe.buffer.writer = BufferedOutput.CapturedBufferedWriter{ - .src = BufferedOutput.WriterSrc{ - .inner = &subproc_readable_ptr.pipe.buffer, - }, - .fd = if (kind == .stdout) bun.STDOUT_FD else bun.STDERR_FD, - .parent = .{ .parent = &subproc_readable_ptr.pipe.buffer }, + return Writable{ + .pipe = pipe, }; - return subproc_readable_ptr.*; - } - - return Readable{ .inherit = {} }; - }, - .path => Readable{ .ignore = {} }, - .dup2, .blob, .fd => Readable{ .fd = fd }, - .array_buffer => { - var subproc_readable_ptr = subproc.getIO(kind); - subproc_readable_ptr.* = Readable{ - .pipe = .{ - .buffer = undefined, - }, - }; - if (stdio.array_buffer.from_jsc) { - BufferedOutput.initWithArrayBuffer(subproc, &subproc_readable_ptr.pipe.buffer, kind, fd, stdio.array_buffer.buf); - } else { - subproc_readable_ptr.pipe.buffer = BufferedOutput.initWithSlice(subproc, kind, fd, stdio.array_buffer.buf.slice()); } - return subproc_readable_ptr.*; - }, - }; - } - - pub fn onClose(this: *Readable, _: ?bun.sys.Error) void { - this.* = .closed; - } - - pub fn onReady(_: *Readable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} - - pub fn onStart(_: *Readable) void {} - - pub fn close(this: *Readable) void { - log("READABLE close", .{}); - switch (this.*) { - .fd => |fd| { - _ = bun.sys.close(fd); - }, - .pipe => { - this.pipe.done(); + return Writable{ .inherit = {} }; }, - else => {}, - } - } - pub fn finalize(this: *Readable) void { - log("Readable::finalize", .{}); - switch (this.*) { - .fd => |fd| { - _ = bun.sys.close(fd); + .blob => |blob| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), + }; }, - .pipe => { - if (this.pipe == .stream and this.pipe.stream.ptr == .File) { - this.close(); - return; - } - - this.pipe.buffer.close(); + .array_buffer => |array_buffer| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), + }; }, - else => {}, - } - } - - pub fn toJS(this: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { - switch (this.*) { .fd => |fd| { - return JSValue.jsNumber(fd); + return Writable{ .fd = fd }; }, - .pipe => { - return this.pipe.toJS(this, globalThis, exited); + .dup2 => |dup2| { + return Writable{ .fd = dup2.to.toFd() }; }, - else => { - return JSValue.jsUndefined(); + .inherit => { + return Writable{ .inherit = {} }; }, - } - } - - pub fn toSlice(this: *Readable) ?[]const u8 { - switch (this.*) { - .fd => return null, - .pipe => { - this.pipe.buffer.fifo.close_on_empty_read = true; - this.pipe.buffer.readAll(); - - const bytes = this.pipe.buffer.internal_buffer.slice(); - // this.pipe.buffer.internal_buffer = .{}; - - if (bytes.len > 0) { - return bytes; - } - - return ""; + .memfd, .path, .ignore => { + return Writable{ .ignore = {} }; }, - else => { - return null; + .capture => { + return Writable{ .ignore = {} }; }, } } + switch (stdio) { + // The shell never uses this + .dup2 => @panic("Unimplemented stdin dup2"), + .pipe => { + // The shell never uses this + @panic("Unimplemented stdin pipe"); + }, - pub fn toBufferedValue(this: *Readable, globalThis: *JSC.JSGlobalObject) JSValue { - switch (this.*) { - .fd => |fd| { - return JSValue.jsNumber(fd); - }, - .pipe => { - this.pipe.buffer.fifo.close_on_empty_read = true; - this.pipe.buffer.readAll(); - - const bytes = this.pipe.buffer.internal_buffer.slice(); - this.pipe.buffer.internal_buffer = .{}; - - if (bytes.len > 0) { - // Return a Buffer so that they can do .toString() on it - return JSC.JSValue.createBuffer(globalThis, bytes, bun.default_allocator); - } - - return JSC.JSValue.createBuffer(globalThis, &.{}, bun.default_allocator); - }, - else => { - return JSValue.jsUndefined(); - }, - } + .blob => |blob| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), + }; + }, + .array_buffer => |array_buffer| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), + }; + }, + .memfd => |memfd| { + std.debug.assert(memfd != bun.invalid_fd); + return Writable{ .memfd = memfd }; + }, + .fd => { + return Writable{ .fd = result.? }; + }, + .inherit => { + return Writable{ .inherit = {} }; + }, + .path, .ignore => { + return Writable{ .ignore = {} }; + }, + .capture => { + return Writable{ .ignore = {} }; + }, } - }; - - pub const BufferedOutput = struct { - fifo: FIFO = undefined, - internal_buffer: bun.ByteList = .{}, - auto_sizer: ?JSC.WebCore.AutoSizer = null, - subproc: *Subprocess, - out_type: OutKind, - /// Sometimes the `internal_buffer` may be filled with memory from JSC, - /// for example an array buffer. In that case we shouldn't dealloc - /// memory and let the GC do it. - from_jsc: bool = false, - status: Status = .{ - .pending = {}, - }, - recall_readall: bool = true, - /// Used to allow to write to fd and also capture the data - writer: ?CapturedBufferedWriter = null, - out: ?*bun.ByteList = null, - - const WriterSrc = struct { - inner: *BufferedOutput, - - pub inline fn bufToWrite(this: WriterSrc, written: usize) []const u8 { - if (written >= this.inner.internal_buffer.len) return ""; - return this.inner.internal_buffer.ptr[written..this.inner.internal_buffer.len]; - } - - pub inline fn isDone(this: WriterSrc, written: usize) bool { - // need to wait for more input - if (this.inner.status != .done and this.inner.status != .err) return false; - return written >= this.inner.internal_buffer.len; - } - }; + } - pub const CapturedBufferedWriter = bun.shell.eval.NewBufferedWriter( - WriterSrc, - struct { - parent: *BufferedOutput, - pub inline fn onDone(this: @This(), e: ?bun.sys.Error) void { - this.parent.onBufferedWriterDone(e); + pub fn toJS(this: *Writable, globalThis: *JSC.JSGlobalObject, subprocess: *Subprocess) JSValue { + return switch (this.*) { + .fd => |fd| JSValue.jsNumber(fd), + .memfd, .ignore => JSValue.jsUndefined(), + .buffer, .inherit => JSValue.jsUndefined(), + .pipe => |pipe| { + this.* = .{ .ignore = {} }; + if (subprocess.process.hasExited() and !subprocess.flags.has_stdin_destructor_called) { + pipe.onAttachedProcessExit(); + return pipe.toJS(globalThis); + } else { + subprocess.flags.has_stdin_destructor_called = false; + subprocess.weak_file_sink_stdin_ptr = pipe; + return pipe.toJSWithDestructor( + globalThis, + JSC.WebCore.SinkDestructor.Ptr.init(subprocess), + ); } }, - EventLoopKind, - ); - - pub const Status = union(enum) { - pending: void, - done: void, - err: bun.sys.Error, }; + } - pub fn init(subproc: *Subprocess, out_type: OutKind, fd: bun.FileDescriptor) BufferedOutput { - return BufferedOutput{ - .out_type = out_type, - .subproc = subproc, - .internal_buffer = .{}, - .fifo = FIFO{ - .fd = fd, - }, - }; - } - - pub fn initWithArrayBuffer(subproc: *Subprocess, out: *BufferedOutput, comptime out_type: OutKind, fd: bun.FileDescriptor, array_buf: JSC.ArrayBuffer.Strong) void { - out.* = BufferedOutput.initWithSlice(subproc, out_type, fd, array_buf.slice()); - out.from_jsc = true; - out.fifo.view = array_buf.held; - out.fifo.buf = out.internal_buffer.ptr[0..out.internal_buffer.cap]; - } - - pub fn initWithSlice(subproc: *Subprocess, comptime out_type: OutKind, fd: bun.FileDescriptor, slice: []u8) BufferedOutput { - return BufferedOutput{ - // fixed capacity - .internal_buffer = bun.ByteList.initWithBuffer(slice), - .auto_sizer = null, - .subproc = subproc, - .fifo = FIFO{ - .fd = fd, - }, - .out_type = out_type, - }; - } - - pub fn initWithAllocator(subproc: *Subprocess, out: *BufferedOutput, comptime out_type: OutKind, allocator: std.mem.Allocator, fd: bun.FileDescriptor, max_size: u32) void { - out.* = init(subproc, out_type, fd); - out.auto_sizer = .{ - .max = max_size, - .allocator = allocator, - .buffer = &out.internal_buffer, - }; - out.fifo.auto_sizer = &out.auto_sizer.?; - } - - pub fn onBufferedWriterDone(this: *BufferedOutput, e: ?bun.sys.Error) void { - _ = e; // autofix - - defer this.signalDoneToCmd(); - // if (e) |err| { - // this.status = .{ .err = err }; - // } - } - - pub fn isDone(this: *BufferedOutput) bool { - if (this.status != .done and this.status != .err) return false; - if (this.writer != null) { - return this.writer.?.isDone(); - } - return true; - } - - pub fn signalDoneToCmd(this: *BufferedOutput) void { - log("signalDoneToCmd ({x}: {s}) isDone={any}", .{ @intFromPtr(this), @tagName(this.out_type), this.isDone() }); - // `this.fifo.close()` will be called from the parent - // this.fifo.close(); - if (!this.isDone()) return; - if (this.subproc.cmd_parent) |cmd| { - if (this.writer != null) { - if (this.writer.?.err) |e| { - if (this.status != .err) { - this.status = .{ .err = e }; - } - } - } - cmd.bufferedOutputClose(this.out_type); - } - } - - /// This is called after it is read (it's confusing because "on read" could - /// be interpreted as present or past tense) - pub fn onRead(this: *BufferedOutput, result: JSC.WebCore.StreamResult) void { - log("ON READ {s} result={s}", .{ @tagName(this.out_type), @tagName(result) }); - defer { - if (this.status == .err or this.status == .done) { - this.signalDoneToCmd(); - } else if (this.recall_readall and this.recall_readall) { - this.readAll(); - } - } - switch (result) { - .pending => { - this.watch(); - return; - }, - .err => |err| { - if (err == .Error) { - this.status = .{ .err = err.Error }; - } else { - this.status = .{ .err = bun.sys.Error.fromCode(.CANCELED, .read) }; - } - // this.fifo.close(); - // this.closeFifoSignalCmd(); - return; - }, - .done => { - this.status = .{ .done = {} }; - // this.fifo.close(); - // this.closeFifoSignalCmd(); - return; - }, - else => { - const slice = switch (result) { - .into_array => this.fifo.buf[0..result.into_array.len], - else => result.slice(), - }; - log("buffered output ({s}) onRead: {s}", .{ @tagName(this.out_type), slice }); - this.internal_buffer.len += @as(u32, @truncate(slice.len)); - if (slice.len > 0) - std.debug.assert(this.internal_buffer.contains(slice)); - - if (this.writer != null) { - this.writer.?.writeIfPossible(false); - } - - this.fifo.buf = this.internal_buffer.ptr[@min(this.internal_buffer.len, this.internal_buffer.cap)..this.internal_buffer.cap]; - - if (result.isDone() or (slice.len == 0 and this.fifo.poll_ref != null and this.fifo.poll_ref.?.isHUP())) { - this.status = .{ .done = {} }; - // this.fifo.close(); - // this.closeFifoSignalCmd(); - } - }, + pub fn finalize(this: *Writable) void { + const subprocess = @fieldParentPtr(Subprocess, "stdin", this); + if (subprocess.this_jsvalue != .zero) { + if (JSC.Codegen.JSSubprocess.stdinGetCached(subprocess.this_jsvalue)) |existing_value| { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); } } - pub fn readAll(this: *BufferedOutput) void { - log("ShellBufferedOutput.readAll doing nothing", .{}); - this.watch(); - } + return switch (this.*) { + .pipe => |pipe| { + pipe.deref(); - pub fn watch(this: *BufferedOutput) void { - std.debug.assert(this.fifo.fd != bun.invalid_fd); - - this.fifo.pending.set(BufferedOutput, this, onRead); - if (!this.fifo.isWatching()) this.fifo.watch(this.fifo.fd); - return; - } + this.* = .{ .ignore = {} }; + }, + .buffer => { + this.buffer.updateRef(false); + this.buffer.deref(); + }, + .memfd => |fd| { + _ = bun.sys.close(fd); + this.* = .{ .ignore = {} }; + }, + .ignore => {}, + .fd, .inherit => {}, + }; + } - pub fn toBlob(this: *BufferedOutput, globalThis: *JSC.JSGlobalObject) JSC.WebCore.Blob { - const blob = JSC.WebCore.Blob.init(this.internal_buffer.slice(), bun.default_allocator, globalThis); - this.internal_buffer = bun.ByteList.init(""); - return blob; + pub fn close(this: *Writable) void { + switch (this.*) { + .pipe => |pipe| { + _ = pipe.end(null); + }, + inline .memfd, .fd => |fd| { + _ = bun.sys.close(fd); + this.* = .{ .ignore = {} }; + }, + .buffer => { + this.buffer.close(); + }, + .ignore => {}, + .inherit => {}, } + } + }; - pub fn toReadableStream(this: *BufferedOutput, globalThis: *JSC.JSGlobalObject, exited: bool) JSC.WebCore.ReadableStream { - if (exited) { - // exited + received EOF => no more read() - if (this.fifo.isClosed()) { - // also no data at all - if (this.internal_buffer.len == 0) { - if (this.internal_buffer.cap > 0) { - if (this.auto_sizer) |auto_sizer| { - this.internal_buffer.deinitWithAllocator(auto_sizer.allocator); - } - } - // so we return an empty stream - return JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.empty(globalThis), - globalThis, - ).?; - } - - return JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.fromBlob( - globalThis, - &this.toBlob(globalThis), - 0, - ), - globalThis, - ).?; - } - } - - { - const internal_buffer = this.internal_buffer; - this.internal_buffer = bun.ByteList.init(""); - - // There could still be data waiting to be read in the pipe - // so we need to create a new stream that will read from the - // pipe and then return the blob. - const result = JSC.WebCore.ReadableStream.fromJS( - JSC.WebCore.ReadableStream.fromFIFO( - globalThis, - &this.fifo, - internal_buffer, - ), - globalThis, - ).?; - this.fifo.fd = bun.invalid_fd; - this.fifo.poll_ref = null; - return result; - } + pub const Readable = union(enum) { + fd: bun.FileDescriptor, + memfd: bun.FileDescriptor, + pipe: *PipeReader, + inherit: void, + ignore: void, + closed: void, + buffer: []u8, + + pub fn ref(this: *Readable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(true); + }, + else => {}, } + } - pub fn close(this: *BufferedOutput) void { - log("BufferedOutput close", .{}); - switch (this.status) { - .done => {}, - .pending => { - this.fifo.close(); - this.status = .{ .done = {} }; - }, - .err => {}, - } - - if (this.internal_buffer.cap > 0 and !this.from_jsc) { - this.internal_buffer.listManaged(bun.default_allocator).deinit(); - this.internal_buffer = .{}; - } + pub fn unref(this: *Readable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(false); + }, + else => {}, } - }; - - pub const BufferedInput = struct { - remain: []const u8 = "", - subproc: *Subprocess, - fd: bun.FileDescriptor = bun.invalid_fd, - poll_ref: ?*Async.FilePoll = null, - written: usize = 0, - - source: union(enum) { - blob: JSC.WebCore.AnyBlob, - array_buffer: JSC.ArrayBuffer.Strong, - }, + } - pub const event_loop_kind = EventLoopKind; - pub usingnamespace JSC.WebCore.NewReadyWatcher(BufferedInput, .writable, onReady); + pub fn toSlice(this: *Readable) ?[]const u8 { + switch (this.*) { + .fd => return null, + .pipe => { + var buf = this.pipe.reader.buffer(); + this.pipe.buffer.fifo.close_on_empty_read = true; + this.pipe.readAll(); - pub fn onReady(this: *BufferedInput, _: i64) void { - if (this.fd == bun.invalid_fd) { - return; - } - - this.write(); - } + const bytes = buf.items[0..]; + // this.pipe.buffer.internal_buffer = .{}; - pub fn writeIfPossible(this: *BufferedInput, comptime is_sync: bool) void { - if (comptime !is_sync) { - - // we ask, "Is it possible to write right now?" - // we do this rather than epoll or kqueue() - // because we don't want to block the thread waiting for the write - switch (bun.isWritable(this.fd)) { - .ready => { - if (this.poll_ref) |poll| { - poll.flags.insert(.writable); - poll.flags.insert(.fifo); - std.debug.assert(poll.flags.contains(.poll_writable)); - } - }, - .hup => { - this.deinit(); - return; - }, - .not_ready => { - if (!this.isWatching()) this.watch(this.fd); - return; - }, + if (bytes.len > 0) { + return bytes; } - } - - this.writeAllowBlocking(is_sync); - } - - pub fn write(this: *BufferedInput) void { - this.writeAllowBlocking(false); - } - - pub fn writeAllowBlocking(this: *BufferedInput, allow_blocking: bool) void { - var to_write = this.remain; - - if (to_write.len == 0) { - // we are done! - this.closeFDIfOpen(); - return; - } - - if (comptime bun.Environment.allow_assert) { - // bun.assertNonBlocking(this.fd); - } - while (to_write.len > 0) { - switch (bun.sys.write(this.fd, to_write)) { - .err => |e| { - if (e.isRetry()) { - log("write({d}) retry", .{ - to_write.len, - }); - - this.watch(this.fd); - this.poll_ref.?.flags.insert(.fifo); - return; - } - - if (e.getErrno() == .PIPE) { - this.deinit(); - return; - } - - // fail - log("write({d}) fail: {d}", .{ to_write.len, e.errno }); - this.deinit(); - return; - }, - - .result => |bytes_written| { - this.written += bytes_written; - - log( - "write({d}) {d}", - .{ - to_write.len, - bytes_written, - }, - ); - - this.remain = this.remain[@min(bytes_written, this.remain.len)..]; - to_write = to_write[bytes_written..]; - - // we are done or it accepts no more input - if (this.remain.len == 0 or (allow_blocking and bytes_written == 0)) { - this.deinit(); - return; - } - }, - } - } + return ""; + }, + .buffer => |buf| buf, + .memfd => @panic("TODO"), + else => { + return null; + }, } + } - fn closeFDIfOpen(this: *BufferedInput) void { - if (this.poll_ref) |poll| { - this.poll_ref = null; - poll.deinit(); - } + pub fn init(out_type: bun.shell.Subprocess.OutKind, stdio: Stdio, event_loop: JSC.EventLoopHandle, process: *ShellSubprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: u32, is_sync: bool) Readable { + _ = allocator; // autofix + _ = max_size; // autofix + _ = is_sync; // autofix - if (this.fd != bun.invalid_fd) { - _ = bun.sys.close(this.fd); - this.fd = bun.invalid_fd; - } - } - - pub fn deinit(this: *BufferedInput) void { - this.closeFDIfOpen(); + assertStdioResult(result); - switch (this.source) { - .blob => |*blob| { - blob.detach(); - }, - .array_buffer => |*array_buffer| { - array_buffer.deinit(); + if (Environment.isWindows) { + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .dup2, .ignore => Readable{ .ignore = {} }, + .path => Readable{ .ignore = {} }, + .fd => |fd| Readable{ .fd = fd }, + // blobs are immutable, so we should only ever get the case + // where the user passed in a Blob with an fd + .blob => Readable{ .ignore = {} }, + .memfd => Readable{ .ignore = {} }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, false, out_type) }, + .array_buffer => { + const readable = Readable{ .pipe = PipeReader.create(event_loop, process, result, false, out_type) }; + readable.pipe.buffered_output = .{ + .array_buffer = .{ .buf = stdio.array_buffer, .i = 0 }, + }; + return readable; }, - } - if (this.subproc.cmd_parent) |cmd| { - cmd.bufferedInputClose(); - } - } - }; - - pub fn getIO(this: *Subprocess, comptime out_kind: OutKind) *Readable { - switch (out_kind) { - .stdout => return &this.stdout, - .stderr => return &this.stderr, + .capture => Readable{ .pipe = PipeReader.create(event_loop, process, result, true, out_type) }, + }; } - } - pub fn hasExited(this: *const Subprocess) bool { - return this.exit_code != null or this.waitpid_err != null or this.signal_code != null; + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .dup2, .ignore => Readable{ .ignore = {} }, + .path => Readable{ .ignore = {} }, + .fd => Readable{ .fd = result.? }, + // blobs are immutable, so we should only ever get the case + // where the user passed in a Blob with an fd + .blob => Readable{ .ignore = {} }, + .memfd => Readable{ .memfd = stdio.memfd }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, false, out_type) }, + .array_buffer => { + const readable = Readable{ .pipe = PipeReader.create(event_loop, process, result, false, out_type) }; + readable.pipe.buffered_output = .{ + .array_buffer = .{ .buf = stdio.array_buffer, .i = 0 }, + }; + return readable; + }, + .capture => Readable{ .pipe = PipeReader.create(event_loop, process, result, true, out_type) }, + }; } - pub fn ref(this: *Subprocess) void { - // const vm = this.globalThis.bunVM(); - - switch (this.poll) { - .poll_ref => if (this.poll.poll_ref) |poll| { - // if (poll.flags.contains(.enable) - poll.ref(GlobalHandle.init(this.globalThis).eventLoopCtx()); + pub fn close(this: *Readable) void { + switch (this.*) { + inline .memfd, .fd => |fd| { + this.* = .{ .closed = {} }; + _ = bun.sys.close(fd); }, - .wait_thread => |*wait_thread| { - wait_thread.poll_ref.ref(GlobalHandle.init(this.globalThis).eventLoopCtx()); + .pipe => { + this.pipe.close(); }, + else => {}, } - - // if (!this.hasCalledGetter(.stdin)) { - this.stdin.ref(); - // } - - // if (!this.hasCalledGetter(.stdout)) { - this.stdout.ref(); - // } - - // if (!this.hasCalledGetter(.stderr)) { - this.stderr.ref(); - // } } - /// This disables the keeping process alive flag on the poll and also in the stdin, stdout, and stderr - pub fn unref(this: *@This(), comptime deactivate_poll_ref: bool) void { - // const vm = this.globalThis.bunVM(); - - switch (this.poll) { - .poll_ref => if (this.poll.poll_ref) |poll| { - if (deactivate_poll_ref) { - poll.onEnded(GlobalHandle.init(this.globalThis).eventLoopCtx()); - } else { - poll.unref(GlobalHandle.init(this.globalThis).eventLoopCtx()); - } + pub fn finalize(this: *Readable) void { + switch (this.*) { + inline .memfd, .fd => |fd| { + this.* = .{ .closed = {} }; + _ = bun.sys.close(fd); }, - .wait_thread => |*wait_thread| { - wait_thread.poll_ref.unref(GlobalHandle.init(this.globalThis).eventLoopCtx()); + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; }, + else => {}, } - // if (!this.hasCalledGetter(.stdin)) { - this.stdin.unref(); - // } - - // if (!this.hasCalledGetter(.stdout)) { - this.stdout.unref(); - // } - - // if (!this.hasCalledGetter(.stderr)) { - this.stdout.unref(); - // } } + }; - pub fn hasKilled(this: *const @This()) bool { - return this.exit_code != null or this.signal_code != null; - } - - pub fn tryKill(this: *@This(), sig: i32) JSC.Maybe(void) { - if (this.hasExited()) { - return .{ .result = {} }; - } - - send_signal: { - if (comptime Environment.isLinux) { - // if these are the same, it means the pidfd is invalid. - if (!WaiterThread.shouldUseWaiterThread()) { - // should this be handled differently? - // this effectively shouldn't happen - if (this.pidfd == bun.invalid_fd) { - return .{ .result = {} }; - } + pub const Flags = packed struct(u3) { + is_sync: bool = false, + killed: bool = false, + waiting_for_onexit: bool = false, + }; + pub const SignalCode = bun.SignalCode; - // first appeared in Linux 5.1 - const rc = std.os.linux.pidfd_send_signal(this.pidfd.cast(), @as(u8, @intCast(sig)), null, 0); + // pub const Pipe = struct { + // writer: Writer = Writer{}, + // parent: *Subprocess, + // src: WriterSrc, - if (rc != 0) { - const errno = std.os.linux.getErrno(rc); + // writer: ?CapturedBufferedWriter = null, - // if the process was already killed don't throw - if (errno != .SRCH and errno != .NOSYS) - return .{ .err = bun.sys.Error.fromCode(errno, .kill) }; - } else { - break :send_signal; - } - } - } - - const err = std.c.kill(this.pid, sig); - if (err != 0) { - const errno = bun.C.getErrno(err); + // status: Status = .{ + // .pending = {}, + // }, + // }; - // if the process was already killed don't throw - if (errno != .SRCH) - return .{ .err = bun.sys.Error.fromCode(errno, .kill) }; - } - } + pub const StaticPipeWriter = JSC.Subprocess.NewStaticPipeWriter(ShellSubprocess); - return .{ .result = {} }; + pub fn getIO(this: *Subprocess, comptime out_kind: OutKind) *Readable { + switch (out_kind) { + .stdout => return &this.stdout, + .stderr => return &this.stderr, } + } - // fn hasCalledGetter(this: *Subprocess, comptime getter: @Type(.EnumLiteral)) bool { - // return this.observable_getters.contains(getter); - // } + pub fn hasExited(this: *const Subprocess) bool { + return this.process.hasExited(); + } - fn closeProcess(this: *@This()) void { - if (comptime !Environment.isLinux) { - return; - } + pub fn ref(this: *Subprocess) void { + this.process.enableKeepingEventLoopAlive(); - const pidfd = this.pidfd; + // this.stdin.ref(); + // } - this.pidfd = bun.invalid_fd; + // if (!this.hasCalledGetter(.stdout)) { + this.stdout.ref(); + // } - if (pidfd != bun.invalid_fd) { - _ = bun.sys.close(pidfd); - } - } + // if (!this.hasCalledGetter(.stderr)) { + this.stderr.ref(); + // } + } - pub fn disconnect(this: *@This()) void { - _ = this; - // if (this.ipc_mode == .none) return; - // this.ipc.socket.close(0, null); - // this.ipc_mode = .none; - } + /// This disables the keeping process alive flag on the poll and also in the stdin, stdout, and stderr + pub fn unref(this: *@This(), comptime deactivate_poll_ref: bool) void { + _ = deactivate_poll_ref; // autofix + // const vm = this.globalThis.bunVM(); - pub fn closeIO(this: *@This(), comptime io: @Type(.EnumLiteral)) void { - if (this.closed.contains(io)) return; - log("close IO {s}", .{@tagName(io)}); - this.closed.insert(io); - - // If you never referenced stdout/stderr, they won't be garbage collected. - // - // That means: - // 1. We need to stop watching them - // 2. We need to free the memory - // 3. We need to halt any pending reads (1) - // if (!this.hasCalledGetter(io)) { - @field(this, @tagName(io)).finalize(); - // } else { - // @field(this, @tagName(io)).close(); - // } - } + this.process.disableKeepingEventLoopAlive(); + // if (!this.hasCalledGetter(.stdin)) { + // this.stdin.unref(); + // } - // This must only be run once per Subprocess - pub fn finalizeSync(this: *@This()) void { - this.closeProcess(); + // if (!this.hasCalledGetter(.stdout)) { + this.stdout.unref(); + // } - this.closeIO(.stdin); - this.closeIO(.stdout); - this.closeIO(.stderr); + // if (!this.hasCalledGetter(.stderr)) { + this.stdout.unref(); + // } + } - // this.exit_promise.deinit(); - // Deinitialization of the shell state is handled by the shell state machine - // this.on_exit_callback.deinit(); - } + pub fn hasKilled(this: *const @This()) bool { + return this.process.hasKilled(); + } - pub fn deinit(this: *@This()) void { - // std.debug.assert(!this.hasPendingActivity()); - this.finalizeSync(); - log("Deinit", .{}); - bun.default_allocator.destroy(this); + pub fn tryKill(this: *@This(), sig: i32) JSC.Maybe(void) { + if (this.hasExited()) { + return .{ .result = {} }; } - // pub fn finalize(this: *Subprocess) callconv(.C) void { - // std.debug.assert(!this.hasPendingActivity()); - // this.finalizeSync(); - // log("Finalize", .{}); - // bun.default_allocator.destroy(this); + return this.process.kill(@intCast(sig)); + } + + // fn hasCalledGetter(this: *Subprocess, comptime getter: @Type(.EnumLiteral)) bool { + // return this.observable_getters.contains(getter); + // } + + fn closeProcess(this: *@This()) void { + this.process.exit_handler = .{}; + this.process.close(); + this.process.deref(); + } + + pub fn disconnect(this: *@This()) void { + _ = this; + // if (this.ipc_mode == .none) return; + // this.ipc.socket.close(0, null); + // this.ipc_mode = .none; + } + + pub fn closeIO(this: *@This(), comptime io: @Type(.EnumLiteral)) void { + if (this.closed.contains(io)) return; + log("close IO {s}", .{@tagName(io)}); + this.closed.insert(io); + + // If you never referenced stdout/stderr, they won't be garbage collected. + // + // That means: + // 1. We need to stop watching them + // 2. We need to free the memory + // 3. We need to halt any pending reads (1) + // if (!this.hasCalledGetter(io)) { + @field(this, @tagName(io)).finalize(); + // } else { + // @field(this, @tagName(io)).close(); // } + } + + // This must only be run once per Subprocess + pub fn finalizeSync(this: *@This()) void { + this.closeProcess(); - pub const SpawnArgs = struct { - arena: *bun.ArenaAllocator, - cmd_parent: ?*ShellCmd = null, + // this.closeIO(.stdin); + this.closeIO(.stdout); + this.closeIO(.stderr); + } - override_env: bool = false, - env_array: std.ArrayListUnmanaged(?[*:0]const u8) = .{ - .items = &.{}, - .capacity = 0, + pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void { + switch (kind) { + .stdin => { + switch (this.stdin) { + .pipe => |pipe| { + pipe.signal.clear(); + pipe.deref(); + this.stdin = .{ .ignore = {} }; + }, + .buffer => { + this.onStaticPipeWriterDone(); + this.stdin.buffer.source.detach(); + this.stdin.buffer.deref(); + this.stdin = .{ .ignore = {} }; + }, + else => {}, + } }, - cwd: []const u8, - stdio: [3]Stdio = .{ - .{ .ignore = {} }, - .{ .pipe = null }, - .{ .inherit = .{} }, + inline .stdout, .stderr => |tag| { + const out: *Readable = &@field(this, @tagName(tag)); + switch (out.*) { + .pipe => |pipe| { + if (pipe.state == .done) { + out.* = .{ .buffer = pipe.state.done }; + pipe.state = .{ .done = &.{} }; + } else { + out.* = .{ .ignore = {} }; + } + pipe.deref(); + }, + else => {}, + } }, - lazy: bool = false, - PATH: []const u8, - argv: std.ArrayListUnmanaged(?[*:0]const u8), - detached: bool, - // ipc_mode: IPCMode, - // ipc_callback: JSValue, - - const EnvMapIter = struct { - map: *bun.DotEnv.Map, - iter: bun.DotEnv.Map.HashTable.Iterator, - alloc: Allocator, - - const Entry = struct { - key: Key, - value: Value, - }; + } + } - pub const Key = struct { - val: []const u8, + pub fn deinit(this: *@This()) void { + this.finalizeSync(); + log("Deinit", .{}); + bun.default_allocator.destroy(this); + } - pub fn format(self: Key, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try writer.writeAll(self.val); - } + pub const SpawnArgs = struct { + arena: *bun.ArenaAllocator, + cmd_parent: ?*ShellCmd = null, - pub fn eqlComptime(this: Key, comptime str: []const u8) bool { - return bun.strings.eqlComptime(this.val, str); - } - }; + override_env: bool = false, + env_array: std.ArrayListUnmanaged(?[*:0]const u8) = .{ + .items = &.{}, + .capacity = 0, + }, + cwd: []const u8, + stdio: [3]Stdio = .{ + .ignore, + .pipe, + .inherit, + }, + lazy: bool = false, + PATH: []const u8, + argv: std.ArrayListUnmanaged(?[*:0]const u8), + detached: bool, + // ipc_mode: IPCMode, + // ipc_callback: JSValue, - pub const Value = struct { - val: [:0]const u8, + const EnvMapIter = struct { + map: *bun.DotEnv.Map, + iter: bun.DotEnv.Map.HashTable.Iterator, + alloc: Allocator, - pub fn format(self: Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - try writer.writeAll(self.val); - } - }; + const Entry = struct { + key: Key, + value: Value, + }; - pub fn init(map: *bun.DotEnv.Map, alloc: Allocator) EnvMapIter { - return EnvMapIter{ - .map = map, - .iter = map.iter(), - .alloc = alloc, - }; - } + pub const Key = struct { + val: []const u8, - pub fn len(this: *const @This()) usize { - return this.map.map.unmanaged.entries.len; + pub fn format(self: Key, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(self.val); } - pub fn next(this: *@This()) !?@This().Entry { - const entry = this.iter.next() orelse return null; - var value = try this.alloc.allocSentinel(u8, entry.value_ptr.value.len, 0); - @memcpy(value[0..entry.value_ptr.value.len], entry.value_ptr.value); - value[entry.value_ptr.value.len] = 0; - return .{ - .key = .{ .val = entry.key_ptr.* }, - .value = .{ .val = value }, - }; + pub fn eqlComptime(this: Key, comptime str: []const u8) bool { + return bun.strings.eqlComptime(this.val, str); } }; - pub fn default(arena: *bun.ArenaAllocator, jsc_vm: GlobalRef, comptime is_sync: bool) SpawnArgs { - var out: SpawnArgs = .{ - .arena = arena, + pub const Value = struct { + val: [:0]const u8, - .override_env = false, - .env_array = .{ - .items = &.{}, - .capacity = 0, - }, - .cwd = GlobalHandle.init(jsc_vm).topLevelDir(), - .stdio = .{ - .{ .ignore = {} }, - .{ .pipe = null }, - .{ .inherit = .{} }, - }, - .lazy = false, - .PATH = GlobalHandle.init(jsc_vm).env().get("PATH") orelse "", - .argv = undefined, - .detached = false, - // .ipc_mode = IPCMode.none, - // .ipc_callback = .zero, + pub fn format(self: Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(self.val); + } + }; + + pub fn init(map: *bun.DotEnv.Map, alloc: Allocator) EnvMapIter { + return EnvMapIter{ + .map = map, + .iter = map.iter(), + .alloc = alloc, }; + } - if (comptime is_sync) { - out.stdio[1] = .{ .pipe = null }; - out.stdio[2] = .{ .pipe = null }; - } - return out; + pub fn len(this: *const @This()) usize { + return this.map.map.unmanaged.entries.len; } - pub fn fillEnvFromProcess(this: *SpawnArgs, globalThis: *JSGlobalObject) void { - var env_iter = EnvMapIter.init(globalThis.bunVM().bundler.env.map, this.arena.allocator()); - return this.fillEnv(globalThis, &env_iter, false); + pub fn next(this: *@This()) !?@This().Entry { + const entry = this.iter.next() orelse return null; + var value = try this.alloc.allocSentinel(u8, entry.value_ptr.value.len, 0); + @memcpy(value[0..entry.value_ptr.value.len], entry.value_ptr.value); + value[entry.value_ptr.value.len] = 0; + return .{ + .key = .{ .val = entry.key_ptr.* }, + .value = .{ .val = value }, + }; } + }; - /// `object_iter` should be a some type with the following fields: - /// - `next() bool` - pub fn fillEnv( - this: *SpawnArgs, - env_iter: *bun.shell.EnvMap.Iterator, - comptime disable_path_lookup_for_arv0: bool, - ) void { - const allocator = this.arena.allocator(); - this.override_env = true; - this.env_array.ensureTotalCapacityPrecise(allocator, env_iter.len) catch bun.outOfMemory(); - - if (disable_path_lookup_for_arv0) { - // If the env object does not include a $PATH, it must disable path lookup for argv[0] - this.PATH = ""; - } + pub fn default(arena: *bun.ArenaAllocator, event_loop: JSC.EventLoopHandle, comptime is_sync: bool) SpawnArgs { + var out: SpawnArgs = .{ + .arena = arena, + + .override_env = false, + .env_array = .{ + .items = &.{}, + .capacity = 0, + }, + .cwd = event_loop.topLevelDir(), + .stdio = .{ + .{ .ignore = {} }, + .{ .pipe = {} }, + .inherit, + }, + .lazy = false, + .PATH = event_loop.env().get("PATH") orelse "", + .argv = undefined, + .detached = false, + // .ipc_mode = IPCMode.none, + // .ipc_callback = .zero, + }; - while (env_iter.next()) |entry| { - const key = entry.key_ptr.*.slice(); - const value = entry.value_ptr.*.slice(); + if (comptime is_sync) { + out.stdio[1] = .{ .pipe = {} }; + out.stdio[2] = .{ .pipe = {} }; + } + return out; + } - var line = std.fmt.allocPrintZ(allocator, "{s}={s}", .{ key, value }) catch bun.outOfMemory(); + pub fn fillEnvFromProcess(this: *SpawnArgs, globalThis: *JSGlobalObject) void { + var env_iter = EnvMapIter.init(globalThis.bunVM().bundler.env.map, this.arena.allocator()); + return this.fillEnv(globalThis, &env_iter, false); + } - if (bun.strings.eqlComptime(key, "PATH")) { - this.PATH = bun.asByteSlice(line["PATH=".len..]); - } + /// `object_iter` should be a some type with the following fields: + /// - `next() bool` + pub fn fillEnv( + this: *SpawnArgs, + env_iter: *bun.shell.EnvMap.Iterator, + comptime disable_path_lookup_for_arv0: bool, + ) void { + const allocator = this.arena.allocator(); + this.override_env = true; + this.env_array.ensureTotalCapacityPrecise(allocator, env_iter.len) catch bun.outOfMemory(); - this.env_array.append(allocator, line) catch bun.outOfMemory(); - } + if (disable_path_lookup_for_arv0) { + // If the env object does not include a $PATH, it must disable path lookup for argv[0] + this.PATH = ""; } - }; - pub const WatchFd = bun.FileDescriptor; + while (env_iter.next()) |entry| { + const key = entry.key_ptr.*.slice(); + const value = entry.value_ptr.*.slice(); - pub fn spawnAsync( - globalThis_: GlobalRef, - spawn_args_: SpawnArgs, - out: **@This(), - ) bun.shell.Result(void) { - const globalThis = GlobalHandle.init(globalThis_); - if (comptime Environment.isWindows) { - return .{ .err = globalThis.throwTODO("spawn() is not yet implemented on Windows") }; + var line = std.fmt.allocPrintZ(allocator, "{s}={s}", .{ key, value }) catch bun.outOfMemory(); + + if (bun.strings.eqlComptime(key, "PATH")) { + this.PATH = bun.asByteSlice(line["PATH=".len..]); + } + + this.env_array.append(allocator, line) catch bun.outOfMemory(); } - var arena = @import("root").bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); + } + }; - var spawn_args = spawn_args_; + pub const WatchFd = bun.FileDescriptor; - var out_watchfd: ?WatchFd = null; + pub fn spawnAsync( + event_loop: JSC.EventLoopHandle, + spawn_args_: SpawnArgs, + out: **@This(), + ) bun.shell.Result(void) { + var arena = @import("root").bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); - const subprocess = switch (spawnMaybeSyncImpl( - .{ - .is_sync = false, - }, - globalThis_, - arena.allocator(), - &out_watchfd, - &spawn_args, - out, - )) { - .result => |subproc| subproc, - .err => |err| return .{ .err = err }, - }; - _ = subprocess; // autofix + var spawn_args = spawn_args_; + + _ = switch (spawnMaybeSyncImpl( + .{ + .is_sync = false, + }, + event_loop, + arena.allocator(), + &spawn_args, + out, + )) { + .result => |subproc| subproc, + .err => |err| return .{ .err = err }, + }; - return bun.shell.Result(void).success; + return bun.shell.Result(void).success; + } + + fn spawnMaybeSyncImpl( + comptime config: struct { + is_sync: bool, + }, + event_loop: JSC.EventLoopHandle, + allocator: Allocator, + spawn_args: *SpawnArgs, + out_subproc: **@This(), + ) bun.shell.Result(*@This()) { + const is_sync = config.is_sync; + + if (!spawn_args.override_env and spawn_args.env_array.items.len == 0) { + // spawn_args.env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); + spawn_args.env_array.items = event_loop.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); + spawn_args.env_array.capacity = spawn_args.env_array.items.len; } - pub fn spawnSync( - globalThis: *JSC.JSGlobalObject, - spawn_args_: SpawnArgs, - ) !?*@This() { - if (comptime Environment.isWindows) { - globalThis.throwTODO("spawn() is not yet implemented on Windows"); - return null; + var should_close_memfd = Environment.isLinux; + + defer { + if (should_close_memfd) { + inline for (0..spawn_args.stdio.len) |fd_index| { + if (spawn_args.stdio[fd_index] == .memfd) { + _ = bun.sys.close(spawn_args.stdio[fd_index].memfd); + spawn_args.stdio[fd_index] = .ignore; + } + } } - const is_sync = true; - var arena = @import("root").bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - var jsc_vm = globalThis.bunVM(); - - var spawn_args = spawn_args_; - - var out_err: ?JSValue = null; - var out_watchfd: if (Environment.isLinux) ?std.os.fd_t else ?i32 = null; - var subprocess = util.spawnMaybeSyncImpl( - .{ - .SpawnArgs = SpawnArgs, - .Subprocess = @This(), - .WaiterThread = WaiterThread, - .is_sync = true, - .is_js = false, + } + + var spawn_options = bun.spawn.SpawnOptions{ + .cwd = spawn_args.cwd, + .stdin = switch (spawn_args.stdio[0].asSpawnOption(0)) { + .result => |opt| opt, + .err => |e| { + return .{ .err = .{ + .custom = bun.default_allocator.dupe(u8, e.toStr()) catch bun.outOfMemory(), + } }; }, - globalThis, - arena.allocator(), - &out_watchfd, - &out_err, - &spawn_args, - ) orelse - { - if (out_err) |err| { - globalThis.throwValue(err); - } - return null; - }; + }, + .stdout = switch (spawn_args.stdio[1].asSpawnOption(1)) { + .result => |opt| opt, + .err => |e| { + return .{ .err = .{ + .custom = bun.default_allocator.dupe(u8, e.toStr()) catch bun.outOfMemory(), + } }; + }, + }, + .stderr = switch (spawn_args.stdio[2].asSpawnOption(2)) { + .result => |opt| opt, + .err => |e| { + return .{ .err = .{ + .custom = bun.default_allocator.dupe(u8, e.toStr()) catch bun.outOfMemory(), + } }; + }, + }, - const out = subprocess.this_jsvalue; + .windows = if (Environment.isWindows) bun.spawn.WindowsSpawnOptions.WindowsOptions{ + .hide_window = true, + .loop = event_loop, + } else {}, + }; - if (comptime !is_sync) { - return out; - } + spawn_args.argv.append(allocator, null) catch { + return .{ .err = .{ .custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory() } }; + }; - if (subprocess.stdin == .buffered_input) { - while (subprocess.stdin.buffered_input.remain.len > 0) { - subprocess.stdin.buffered_input.writeIfPossible(true); - } - } - subprocess.closeIO(.stdin); + spawn_args.env_array.append(allocator, null) catch { + return .{ .err = .{ .custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory() } }; + }; - const watchfd = out_watchfd orelse { - globalThis.throw("watchfd is null", .{}); - return null; - }; + var spawn_result = switch (bun.spawn.spawnProcess( + &spawn_options, + @ptrCast(spawn_args.argv.items.ptr), + @ptrCast(spawn_args.env_array.items.ptr), + ) catch |err| { + return .{ .err = .{ .custom = std.fmt.allocPrint(bun.default_allocator, "Failed to spawn process: {s}", .{@errorName(err)}) catch bun.outOfMemory() } }; + }) { + .err => |err| return .{ .err = .{ .sys = err.toSystemError() } }, + .result => |result| result, + }; - if (!WaiterThread.shouldUseWaiterThread()) { - const poll = Async.FilePoll.init(jsc_vm, watchfd, .{}, @This(), subprocess); - subprocess.poll = .{ .poll_ref = poll }; - switch (subprocess.poll.poll_ref.?.register( - jsc_vm.event_loop_handle.?, - .process, - true, - )) { - .result => { - subprocess.poll.poll_ref.?.enableKeepingProcessAlive(jsc_vm); - }, - .err => |err| { - if (err.getErrno() != .SRCH) { - @panic("This shouldn't happen"); - } + var subprocess = event_loop.allocator().create(Subprocess) catch bun.outOfMemory(); + out_subproc.* = subprocess; + subprocess.* = Subprocess{ + .event_loop = event_loop, + .process = spawn_result.toProcess( + event_loop, + is_sync, + ), + .stdin = Subprocess.Writable.init(spawn_args.stdio[0], event_loop, subprocess, spawn_result.stdin) catch bun.outOfMemory(), + + .stdout = Subprocess.Readable.init(.stdout, spawn_args.stdio[1], event_loop, subprocess, spawn_result.stdout, event_loop.allocator(), ShellSubprocess.default_max_buffer_size, true), + .stderr = Subprocess.Readable.init(.stderr, spawn_args.stdio[2], event_loop, subprocess, spawn_result.stderr, event_loop.allocator(), ShellSubprocess.default_max_buffer_size, true), + + .flags = .{ + .is_sync = is_sync, + }, + .cmd_parent = spawn_args.cmd_parent, + }; + subprocess.process.setExitHandler(subprocess); - // process has already exited - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.onExitNotification(); - }, - } - } else { - WaiterThread.appendShell( - Subprocess, - subprocess, - ); + if (subprocess.stdin == .pipe) { + subprocess.stdin.pipe.signal = JSC.WebCore.Signal.init(&subprocess.stdin); + } + + var send_exit_notification = false; + + if (comptime !is_sync) { + switch (subprocess.process.watch(event_loop)) { + .result => {}, + .err => { + send_exit_notification = true; + spawn_args.lazy = false; + }, } + } - while (!subprocess.hasExited()) { - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - subprocess.stderr.pipe.buffer.readAll(); - } + defer { + if (send_exit_notification) { + // process has already exited + // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 + subprocess.wait(subprocess.flags.is_sync); + } + } - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - subprocess.stdout.pipe.buffer.readAll(); - } + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.start().assert(); + } - jsc_vm.tick(); - jsc_vm.eventLoop().autoTick(); + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.start(subprocess, event_loop).assert(); + if ((is_sync or !spawn_args.lazy) and subprocess.stdout == .pipe) { + subprocess.stdout.pipe.readAll(); } + } - return subprocess; + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.start(subprocess, event_loop).assert(); + + if ((is_sync or !spawn_args.lazy) and subprocess.stderr == .pipe) { + subprocess.stderr.pipe.readAll(); + } } - pub fn spawnMaybeSyncImpl( - comptime config: struct { - is_sync: bool, - }, - globalThis_: GlobalRef, - allocator: Allocator, - out_watchfd: *?WatchFd, - spawn_args: *SpawnArgs, - out_subproc: **@This(), - ) bun.shell.Result(*@This()) { - const globalThis = GlobalHandle.init(globalThis_); - const is_sync = config.is_sync; - - var env: [*:null]?[*:0]const u8 = undefined; - - var attr = PosixSpawn.Attr.init() catch { - return .{ .err = globalThis.throw("out of memory", .{}) }; - }; + should_close_memfd = false; - var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK; + log("returning", .{}); - if (comptime Environment.isMac) { - flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; + return .{ .result = subprocess }; + } + + pub fn wait(this: *@This(), sync: bool) void { + return this.process.wait(sync); + } + + pub fn onProcessExit(this: *@This(), _: *Process, status: bun.spawn.Status, _: *const bun.spawn.Rusage) void { + log("onProcessExit({x}, {any})", .{ @intFromPtr(this), status }); + const exit_code: ?u8 = brk: { + if (status == .exited) { + break :brk status.exited.code; } - if (spawn_args.detached) { - flags |= bun.C.POSIX_SPAWN_SETSID; + if (status == .err) { + // TODO: handle error } - defer attr.deinit(); - var actions = PosixSpawn.Actions.init() catch |err| { - return .{ .err = globalThis.handleError(err, "in posix_spawn") }; - }; - if (comptime Environment.isMac) { - attr.set(@intCast(flags)) catch |err| { - return .{ .err = globalThis.handleError(err, "in posix_spawn") }; - }; - } else if (comptime Environment.isLinux) { - attr.set(@intCast(flags)) catch |err| { - return .{ .err = globalThis.handleError(err, "in posix_spawn") }; - }; + if (status == .signaled) { + if (status.signalCode()) |code| { + break :brk code.toExitCode().?; + } } - attr.resetSignals() catch { - return .{ .err = globalThis.throw("Failed to reset signals in posix_spawn", .{}) }; - }; + break :brk null; + }; - defer actions.deinit(); + if (exit_code) |code| { + if (this.cmd_parent) |cmd| { + if (cmd.exit_code == null) { + cmd.onExit(code); + } + } + } + } + + const os = std.os; +}; + +const WaiterThread = bun.spawn.WaiterThread; + +pub const PipeReader = struct { + reader: IOReader = undefined, + process: ?*ShellSubprocess = null, + event_loop: JSC.EventLoopHandle = undefined, + state: union(enum) { + pending: void, + done: []u8, + err: bun.sys.Error, + } = .{ .pending = {} }, + stdio_result: StdioResult, + out_type: bun.shell.subproc.ShellSubprocess.OutKind, + captured_writer: CapturedWriter = .{}, + buffered_output: BufferedOutput = .{ .bytelist = .{} }, + ref_count: u32 = 1, + + const BufferedOutput = union(enum) { + bytelist: bun.ByteList, + array_buffer: struct { + buf: JSC.ArrayBuffer.Strong, + i: u32 = 0, + }, + + pub fn slice(this: *BufferedOutput) []const u8 { + return switch (this.*) { + .bytelist => this.bytelist.slice(), + .array_buffer => this.array_buffer.buf.slice(), + }; + } - if (!spawn_args.override_env and spawn_args.env_array.items.len == 0) { - // spawn_args.env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); - spawn_args.env_array.items = globalThis.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory(); - spawn_args.env_array.capacity = spawn_args.env_array.items.len; + pub fn append(this: *BufferedOutput, bytes: []const u8) void { + switch (this.*) { + .bytelist => { + this.bytelist.append(bun.default_allocator, bytes) catch bun.outOfMemory(); + }, + .array_buffer => { + const array_buf_slice = this.array_buffer.buf.slice(); + // TODO: We should probably throw error here? + if (this.array_buffer.i >= array_buf_slice.len) return; + const len = @min(array_buf_slice.len - this.array_buffer.i, bytes.len); + @memcpy(array_buf_slice[this.array_buffer.i .. this.array_buffer.i + len], bytes[0..len]); + this.array_buffer.i += @intCast(len); + }, } + } - const stdin_pipe = if (spawn_args.stdio[0].isPiped()) bun.sys.pipe().unwrap() catch |err| { - return .{ .err = globalThis.throw("failed to create stdin pipe: {s}", .{@errorName(err)}) }; - } else undefined; - - const stdout_pipe = if (spawn_args.stdio[1].isPiped()) bun.sys.pipe().unwrap() catch |err| { - return .{ .err = globalThis.throw("failed to create stdout pipe: {s}", .{@errorName(err)}) }; - } else undefined; - - const stderr_pipe = if (spawn_args.stdio[2].isPiped()) bun.sys.pipe().unwrap() catch |err| { - return .{ .err = globalThis.throw("failed to create stderr pipe: {s}", .{@errorName(err)}) }; - } else undefined; - - spawn_args.stdio[0].setUpChildIoPosixSpawn( - &actions, - stdin_pipe, - stderr_pipe, - bun.STDIN_FD, - ) catch |err| { - return .{ .err = globalThis.handleError(err, "in configuring child stdin") }; - }; + pub fn deinit(this: *BufferedOutput) void { + switch (this.*) { + .bytelist => { + this.bytelist.deinitWithAllocator(bun.default_allocator); + }, + .array_buffer => {}, + } + } + }; - spawn_args.stdio[1].setUpChildIoPosixSpawn( - &actions, - stdout_pipe, - stderr_pipe, - bun.STDOUT_FD, - ) catch |err| { - return .{ .err = globalThis.handleError(err, "in configuring child stdout") }; - }; + pub usingnamespace bun.NewRefCounted(PipeReader, deinit); - spawn_args.stdio[2].setUpChildIoPosixSpawn( - &actions, - stderr_pipe, - stderr_pipe, - bun.STDERR_FD, - ) catch |err| { - return .{ .err = globalThis.handleError(err, "in configuring child stderr") }; - }; + pub const CapturedWriter = struct { + dead: bool = true, + writer: IOWriter = .{}, + written: usize = 0, + err: ?bun.sys.Error = null, - actions.chdir(spawn_args.cwd) catch |err| { - return .{ .err = globalThis.handleError(err, "in chdir()") }; - }; + pub const IOWriter = bun.io.StreamingWriter( + CapturedWriter, + onWrite, + onError, + null, + onClose, + ); - spawn_args.argv.append(allocator, null) catch { - return .{ .err = globalThis.throw("out of memory", .{}) }; - }; + pub const Poll = IOWriter; - // // IPC is currently implemented in a very limited way. - // // - // // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special - // // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). - // // - // // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional - // // - // // And then fd 3 is assigned specifically and only for IPC. This is quite lame, because Node.js allows - // // the ipc fd to be any number and it just works. But most people only care about the default `.fork()` - // // behavior, where this workaround suffices. - // // - // // When Bun.spawn() is given a `.onMessage` callback, it enables IPC as follows: - // var socket: if (is_js) IPC.Socket else u0 = undefined; - // if (comptime is_js) { - // if (spawn_args.ipc_mode != .none) { - // if (comptime is_sync) { - // globalThis.throwInvalidArguments("IPC is not supported in Bun.spawnSync", .{}); - // return null; - // } + pub fn getBuffer(this: *CapturedWriter) []const u8 { + const p = this.parent(); + if (this.written >= p.reader.buffer().items.len) return ""; + return p.reader.buffer().items[this.written..]; + } - // spawn_args.env_array.ensureUnusedCapacity(allocator, 2) catch |err| { - // out_err.* = globalThis.handleError(err, "in posix_spawn"); - // return null; - // }; - // spawn_args.env_array.appendAssumeCapacity("BUN_INTERNAL_IPC_FD=3"); - - // var fds: [2]uws.LIBUS_SOCKET_DESCRIPTOR = undefined; - // socket = uws.newSocketFromPair( - // jsc_vm.rareData().spawnIPCContext(jsc_vm), - // @sizeOf(*Subprocess), - // &fds, - // ) orelse { - // globalThis.throw("failed to create socket pair: E{s}", .{ - // @tagName(bun.sys.getErrno(-1)), - // }); - // return null; - // }; - // actions.dup2(fds[1], 3) catch |err| { - // out_err.* = globalThis.handleError(err, "in posix_spawn"); - // return null; - // }; - // } - // } + pub fn loop(this: *CapturedWriter) *uws.Loop { + return this.parent().event_loop.loop(); + } - spawn_args.env_array.append(allocator, null) catch { - return .{ .err = globalThis.throw("out of memory", .{}) }; - }; - env = @as(@TypeOf(env), @ptrCast(spawn_args.env_array.items.ptr)); + pub fn parent(this: *CapturedWriter) *PipeReader { + return @fieldParentPtr(PipeReader, "captured_writer", this); + } - const pid = brk: { - defer { - if (spawn_args.stdio[0].isPiped()) { - _ = bun.sys.close(stdin_pipe[0]); - } + pub fn eventLoop(this: *CapturedWriter) JSC.EventLoopHandle { + return this.parent().eventLoop(); + } - if (spawn_args.stdio[1].isPiped()) { - _ = bun.sys.close(stdout_pipe[1]); - } + pub fn isDone(this: *CapturedWriter, just_written: usize) bool { + if (this.dead) return true; + if (this.writer.is_done) return true; + const p = this.parent(); + if (p.state == .pending) return false; + return this.written + just_written >= p.buffered_output.slice().len; + } - if (spawn_args.stdio[2].isPiped()) { - _ = bun.sys.close(stderr_pipe[1]); - } - } + pub fn onWrite(this: *CapturedWriter, amount: usize, status: bun.io.WriteStatus) void { + log("CapturedWriter({x}, {s}) onWrite({d}, {any}) total_written={d} total_to_write={d}", .{ @intFromPtr(this), @tagName(this.parent().out_type), amount, status, this.written + amount, this.parent().buffered_output.slice().len }); + this.written += amount; + // TODO: @zackradisic is this right? + if (status == .end_of_file) return; + if (this.written >= this.parent().buffered_output.slice().len) { + this.writer.end(); + } + } - log("spawning", .{}); - break :brk switch (PosixSpawn.spawnZ(spawn_args.argv.items[0].?, actions, attr, @as([*:null]?[*:0]const u8, @ptrCast(spawn_args.argv.items[0..].ptr)), env)) { - .err => |err| { - log("error spawning", .{}); - return .{ .err = .{ .sys = err.toSystemError() } }; - }, - .result => |pid_| pid_, - }; - }; + pub fn onError(this: *CapturedWriter, err: bun.sys.Error) void { + this.err = err; + } - const pidfd: std.os.fd_t = brk: { - if (!Environment.isLinux or WaiterThread.shouldUseWaiterThread()) { - break :brk pid; - } + pub fn onClose(this: *CapturedWriter) void { + log("CapturedWriter({x}, {s}) onClose()", .{ @intFromPtr(this), @tagName(this.parent().out_type) }); + this.parent().onCapturedWriterDone(); + } + }; - var pidfd_flags = JSC.Subprocess.pidfdFlagsForLinux(); - - var rc = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - while (true) { - switch (std.os.linux.getErrno(rc)) { - .SUCCESS => break :brk @as(std.os.fd_t, @intCast(rc)), - .INTR => { - rc = std.os.linux.pidfd_open( - @intCast(pid), - pidfd_flags, - ); - continue; - }, - else => |err| { - if (err == .INVAL) { - if (pidfd_flags != 0) { - rc = std.os.linux.pidfd_open( - @intCast(pid), - 0, - ); - pidfd_flags = 0; - continue; - } - } + pub const IOReader = bun.io.BufferedReader; + pub const Poll = IOReader; + + pub fn detach(this: *PipeReader) void { + log("PipeReader(0x{x}, {s}) detach()", .{ @intFromPtr(this), @tagName(this.out_type) }); + this.process = null; + this.deref(); + } + + pub fn isDone(this: *PipeReader) bool { + log("PipeReader(0x{x}, {s}) isDone() state={s} captured_writer_done={any}", .{ @intFromPtr(this), @tagName(this.out_type), @tagName(this.state), this.captured_writer.isDone(0) }); + if (this.state == .pending) return false; + return this.captured_writer.isDone(0); + } + + pub fn onCapturedWriterDone(this: *PipeReader) void { + this.signalDoneToCmd(); + } + + pub fn create(event_loop: JSC.EventLoopHandle, process: *ShellSubprocess, result: StdioResult, comptime capture: bool, out_type: bun.shell.Subprocess.OutKind) *PipeReader { + var this: *PipeReader = PipeReader.new(.{ + .process = process, + .reader = IOReader.init(@This()), + .event_loop = event_loop, + .stdio_result = result, + .out_type = out_type, + }); + log("PipeReader(0x{x}, {s}) create()", .{ @intFromPtr(this), @tagName(this.out_type) }); + + if (capture) { + this.captured_writer.dead = false; + this.captured_writer.writer.setParent(&this.captured_writer); + } - const error_instance = brk2: { - if (err == .NOSYS) { - WaiterThread.setShouldUseWaiterThread(); - break :brk pid; - } + if (Environment.isWindows) { + this.reader.source = + switch (result) { + .buffer => .{ .pipe = this.stdio_result.buffer }, + .buffer_fd => .{ .file = bun.io.Source.openFile(this.stdio_result.buffer_fd) }, + .unavailable => @panic("Shouldn't happen."), + }; + } + this.reader.setParent(this); + + return this; + } + + pub fn readAll(this: *PipeReader) void { + if (this.state == .pending) + this.reader.read(); + } + + pub fn start(this: *PipeReader, process: *ShellSubprocess, event_loop: JSC.EventLoopHandle) JSC.Maybe(void) { + // this.ref(); + this.process = process; + this.event_loop = event_loop; + if (Environment.isWindows) { + return this.reader.startWithCurrentPipe(); + } - break :brk2 bun.sys.Error.fromCode(err, .open); - }; - var status: u32 = 0; - // ensure we don't leak the child process on error - _ = std.os.linux.wait4(pid, &status, 0, null); - log("Error in getting pidfd", .{}); - return .{ .err = .{ .sys = error_instance.toSystemError() } }; - }, - } + switch (this.reader.start(this.stdio_result.?, true)) { + .err => |err| { + return .{ .err = err }; + }, + .result => { + if (comptime Environment.isPosix) { + // TODO: are these flags correct + const poll = this.reader.handle.poll; + poll.flags.insert(.nonblocking); + poll.flags.insert(.socket); } - }; - var subprocess = globalThis.allocator().create(Subprocess) catch bun.outOfMemory(); - out_subproc.* = subprocess; - subprocess.* = Subprocess{ - .globalThis = globalThis_, - .pid = pid, - .pidfd = if (Environment.isLinux and WaiterThread.shouldUseWaiterThread()) bun.toFD(pidfd) else if (Environment.isLinux) bun.invalid_fd else 0, - .stdin = Subprocess.Writable.init(subprocess, spawn_args.stdio[0], stdin_pipe[1], globalThis_) catch bun.outOfMemory(), - // Readable initialization functions won't touch the subrpocess pointer so it's okay to hand it to them even though it technically has undefined memory at the point of Readble initialization - // stdout and stderr only uses allocator and default_max_buffer_size if they are pipes and not a array buffer - .stdout = Subprocess.Readable.init(subprocess, .stdout, spawn_args.stdio[1], stdout_pipe[0], globalThis.getAllocator(), Subprocess.default_max_buffer_size), - .stderr = Subprocess.Readable.init(subprocess, .stderr, spawn_args.stdio[2], stderr_pipe[0], globalThis.getAllocator(), Subprocess.default_max_buffer_size), - .flags = .{ - .is_sync = is_sync, - }, - .cmd_parent = spawn_args.cmd_parent, - }; + return .{ .result = {} }; + }, + } + } - if (subprocess.stdin == .pipe) { - subprocess.stdin.pipe.signal = JSC.WebCore.Signal.init(&subprocess.stdin); - } + pub const toJS = toReadableStream; - var send_exit_notification = false; - const watchfd = bun.toFD(if (comptime Environment.isLinux) brk: { - break :brk pidfd; - } else brk: { - break :brk pid; - }); - out_watchfd.* = bun.toFD(watchfd); - - if (comptime !is_sync) { - if (!WaiterThread.shouldUseWaiterThread()) { - const poll = Async.FilePoll.init(globalThis.eventLoopCtx(), watchfd, .{}, Subprocess, subprocess); - subprocess.poll = .{ .poll_ref = poll }; - switch (subprocess.poll.poll_ref.?.register( - // jsc_vm.event_loop_handle.?, - JSC.AbstractVM(globalThis.eventLoopCtx()).platformEventLoop(), - .process, - true, - )) { - .result => { - subprocess.poll.poll_ref.?.enableKeepingProcessAlive(globalThis.eventLoopCtx()); - }, - .err => |err| { - if (err.getErrno() != .SRCH) { - @panic("This shouldn't happen"); - } + pub fn onReadChunk(ptr: *anyopaque, chunk: []const u8, has_more: bun.io.ReadState) bool { + var this: *PipeReader = @ptrCast(@alignCast(ptr)); + this.buffered_output.append(chunk); + log("PipeReader(0x{x}, {s}) onReadChunk(chunk_len={d}, has_more={s})", .{ @intFromPtr(this), @tagName(this.out_type), chunk.len, @tagName(has_more) }); - send_exit_notification = true; - spawn_args.lazy = false; - }, + // Setup the writer + if (!this.captured_writer.dead) { + // FIXME: Can't use bun.STDOUT_FD and bun.STDERR_FD here because we could have multiple writers to it and break kqueue/epoll + const writer_fd: bun.FileDescriptor = if (this.out_type == .stdout) bun.shell.STDOUT_FD else bun.shell.STDERR_FD; + + if (comptime Environment.isWindows) { + if (this.captured_writer.writer.source == null) { + if (this.captured_writer.writer.start(writer_fd, true).asErr()) |e| { + _ = e; // autofix + Output.panic("TODO SHELL SUBPROC onReadChunk error", .{}); } - } else { - WaiterThread.appendShell(Subprocess, subprocess); } - } - defer { - if (send_exit_notification) { - // process has already exited - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.wait(subprocess.flags.is_sync); + this.captured_writer.writer.outgoing.write(chunk) catch bun.outOfMemory(); + } else if (this.captured_writer.writer.getPoll() == null) { + if (this.captured_writer.writer.start(writer_fd, true).asErr()) |e| { + _ = e; // autofix + Output.panic("TODO SHELL SUBPROC onReadChunk error", .{}); } } - if (subprocess.stdin == .buffered_input) { - subprocess.stdin.buffered_input.remain = switch (subprocess.stdin.buffered_input.source) { - .blob => subprocess.stdin.buffered_input.source.blob.slice(), - .array_buffer => |array_buffer| array_buffer.slice(), - }; - subprocess.stdin.buffered_input.writeIfPossible(is_sync); - } + // if (this.captured_writer.writer.start(writer_fd, true).asErr()) |e| { + // const writer = std.io.getStdOut().writer(); + // e.format("Yoops ", .{}, writer) catch @panic("oops"); + // @panic("TODO SHELL SUBPROC onReadChunk error"); + // } - if (subprocess.stdout == .pipe and subprocess.stdout.pipe == .buffer) { - log("stdout readall", .{}); - if (comptime is_sync) { - subprocess.stdout.pipe.buffer.readAll(); - } else if (!spawn_args.lazy) { - subprocess.stdout.pipe.buffer.readAll(); - } - } + // if (comptime Environment.isWindows) { + // if (this.captured_writer.writer.source == null) { + // if (this.captured_writer.writer.start(writer_fd, true).asErr()) |e| { + // const writer = std.io.getStdOut().writer(); + // e.format("Yoops ", .{}, writer) catch @panic("oops"); + // @panic("TODO SHELL SUBPROC onReadChunk error"); + // } + // } + // } else { + // if (this.captured_writer.writer.getPoll() == null) { + // this.captured_writer.writer.handle = .{ .poll = Async.FilePoll.init(this.eventLoop(), writer_fd, .{}, @TypeOf(this.captured_writer.writer), &this.captured_writer.writer) }; + // } + // } - if (subprocess.stderr == .pipe and subprocess.stderr.pipe == .buffer) { - log("stderr readall", .{}); - if (comptime is_sync) { - subprocess.stderr.pipe.buffer.readAll(); - } else if (!spawn_args.lazy) { - subprocess.stderr.pipe.buffer.readAll(); - } + log("CapturedWriter(0x{x}, {s}) write", .{ @intFromPtr(&this.captured_writer), @tagName(this.out_type) }); + if (bun.Environment.isWindows) { + _ = this.captured_writer.writer.flush(); + } else switch (this.captured_writer.writer.write(chunk)) { + .err => |e| { + const writer = std.io.getStdOut().writer(); + e.format("Yoops ", .{}, writer) catch @panic("oops"); + @panic("TODO SHELL SUBPROC onReadChunk error"); + }, + else => |result| { + log("CapturedWriter(0x{x}, {s}) write result={any}", .{ @intFromPtr(&this.captured_writer), @tagName(this.out_type), result }); + }, } - log("returning", .{}); - - return .{ .result = subprocess }; } - pub fn onExitNotificationTask(this: *@This()) void { - // var vm = this.globalThis.bunVM(); - const is_sync = this.flags.is_sync; + const should_continue = has_more != .eof; - defer { - // if (!is_sync) - // vm.drainMicrotasks(); - if (!is_sync) { - if (comptime EventLoopKind == .js) this.globalThis.bunVM().drainMicrotasks(); - } + if (should_continue) { + if (bun.Environment.isPosix) this.reader.registerPoll() else switch (this.reader.startWithCurrentPipe()) { + .err => |e| { + const writer = std.io.getStdOut().writer(); + e.format("Yoops ", .{}, writer) catch @panic("oops"); + @panic("TODO SHELL SUBPROC onReadChunk error"); + }, + else => {}, } - this.wait(false); } - pub fn onExitNotification( - this: *@This(), - ) void { - std.debug.assert(this.flags.is_sync); - - this.wait(this.flags.is_sync); + return should_continue; + } + + pub fn onReaderDone(this: *PipeReader) void { + log("onReaderDone(0x{x}, {s})", .{ @intFromPtr(this), @tagName(this.out_type) }); + const owned = this.toOwnedSlice(); + this.state = .{ .done = owned }; + if (!this.isDone()) return; + this.signalDoneToCmd(); + if (this.process) |process| { + // this.process = null; + process.onCloseIO(this.kind(process)); + this.deref(); } - - pub fn wait(this: *@This(), sync: bool) void { - return this.onWaitPid(sync, PosixSpawn.waitpid(this.pid, if (sync) 0 else std.os.W.NOHANG)); + } + + pub fn signalDoneToCmd( + this: *PipeReader, + ) void { + if (!this.isDone()) return; + log("signalDoneToCmd ({x}: {s}) isDone={any}", .{ @intFromPtr(this), @tagName(this.out_type), this.isDone() }); + if (bun.Environment.allow_assert) std.debug.assert(this.process != null); + if (this.process) |proc| { + if (proc.cmd_parent) |cmd| { + if (this.captured_writer.err) |e| { + if (this.state != .err) { + this.state = .{ .err = e }; + } + } + cmd.bufferedOutputClose(this.out_type); + } } + } - pub fn watch(this: *@This()) JSC.Maybe(void) { - if (WaiterThread.shouldUseWaiterThread()) { - WaiterThread.appendShell(@This(), this); - return JSC.Maybe(void){ .result = {} }; - } + pub fn kind(reader: *const PipeReader, process: *const ShellSubprocess) StdioKind { + if (process.stdout == .pipe and process.stdout.pipe == reader) { + return .stdout; + } - if (this.poll.poll_ref) |poll| { - var global_handle = GlobalHandle.init(this.globalThis); - var event_loop_ctx = JSC.AbstractVM(global_handle.eventLoopCtx()); - const registration = poll.register( - // this.globalThis.bunVM().event_loop_handle.?, - event_loop_ctx.platformEventLoop(), - .process, - true, - ); - - return registration; - } else { - @panic("Internal Bun error: poll_ref in Subprocess is null unexpectedly. Please file a bug report."); - } + if (process.stderr == .pipe and process.stderr.pipe == reader) { + return .stderr; } - pub fn onWaitPid(this: *@This(), sync: bool, waitpid_result_: JSC.Maybe(PosixSpawn.WaitPidResult)) void { - if (Environment.isWindows) { - @panic("windows doesnt support subprocess yet. haha"); - } - // defer if (sync) this.updateHasPendingActivity(); + @panic("We should be either stdout or stderr"); + } - const pid = this.pid; + pub fn takeBuffer(this: *PipeReader) std.ArrayList(u8) { + return this.reader.takeBuffer(); + } - var waitpid_result = waitpid_result_; + pub fn slice(this: *PipeReader) []const u8 { + return this.buffered_output.slice(); + } - while (true) { - switch (waitpid_result) { - .err => |err| { - this.waitpid_err = err; - }, - .result => |result| { - if (result.pid == pid) { - if (std.os.W.IFEXITED(result.status)) { - this.exit_code = @as(u8, @truncate(std.os.W.EXITSTATUS(result.status))); - } + pub fn toOwnedSlice(this: *PipeReader) []u8 { + if (this.state == .done) { + return this.state.done; + } + // we do not use .toOwnedSlice() because we don't want to reallocate memory. + const out = this.reader._buffer; + this.reader._buffer.items = &.{}; + this.reader._buffer.capacity = 0; + return out.items; + } + + pub fn updateRef(this: *PipeReader, add: bool) void { + this.reader.updateRef(add); + } + + pub fn watch(this: *PipeReader) void { + if (!this.reader.isDone()) + this.reader.watch(); + } + + pub fn toReadableStream(this: *PipeReader, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + defer this.deinit(); + + switch (this.state) { + .pending => { + const stream = JSC.WebCore.ReadableStream.fromPipe(globalObject, this, &this.reader); + this.state = .{ .done = &.{} }; + return stream; + }, + .done => |bytes| { + const blob = JSC.WebCore.Blob.init(bytes, bun.default_allocator, globalObject); + this.state = .{ .done = &.{} }; + return JSC.WebCore.ReadableStream.fromBlob(globalObject, &blob, 0); + }, + .err => |err| { + _ = err; // autofix + const empty = JSC.WebCore.ReadableStream.empty(globalObject); + JSC.WebCore.ReadableStream.cancel(&JSC.WebCore.ReadableStream.fromJS(empty, globalObject).?, globalObject); + return empty; + }, + } + } - // True if the process terminated due to receipt of a signal. - if (std.os.W.IFSIGNALED(result.status)) { - this.signal_code = @as(SignalCode, @enumFromInt(@as(u8, @truncate(std.os.W.TERMSIG(result.status))))); - } else if ( - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/waitpid.2.html - // True if the process has not terminated, but has stopped and can - // be restarted. This macro can be true only if the wait call spec-ified specified - // ified the WUNTRACED option or if the child process is being - // traced (see ptrace(2)). - std.os.W.IFSTOPPED(result.status)) { - this.signal_code = @as(SignalCode, @enumFromInt(@as(u8, @truncate(std.os.W.STOPSIG(result.status))))); - } - } + pub fn toBuffer(this: *PipeReader, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + switch (this.state) { + .done => |bytes| { + defer this.state = .{ .done = &.{} }; + return JSC.MarkedArrayBuffer.fromBytes(bytes, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); + }, + else => { + return JSC.JSValue.undefined; + }, + } + } - if (!this.hasExited()) { - switch (this.watch()) { - .result => {}, - .err => |err| { - if (comptime Environment.isMac) { - if (err.getErrno() == .SRCH) { - waitpid_result = PosixSpawn.waitpid(pid, if (sync) 0 else std.os.W.NOHANG); - continue; - } - } - }, - } - } - }, - } - break; - } + pub fn onReaderError(this: *PipeReader, err: bun.sys.Error) void { + log("PipeReader(0x{x}) onReaderError {}", .{ @intFromPtr(this), err }); + if (this.state == .done) { + bun.default_allocator.free(this.state.done); + } + this.state = .{ .err = err }; + this.signalDoneToCmd(); + if (this.process) |process| { + // this.process = null; + process.onCloseIO(this.kind(process)); + this.deref(); + } + } - if (!sync and this.hasExited()) { - // const vm = this.globalThis.bunVM(); + pub fn close(this: *PipeReader) void { + switch (this.state) { + .pending => { + this.reader.close(); + }, + .done => {}, + .err => {}, + } + } - // prevent duplicate notifications - switch (this.poll) { - .poll_ref => |poll_| { - if (poll_) |poll| { - this.poll.poll_ref = null; - // poll.deinitWithVM(vm); + pub fn eventLoop(this: *PipeReader) JSC.EventLoopHandle { + return this.event_loop; + } - poll.deinitWithVM(GlobalHandle.init(this.globalThis).eventLoopCtx()); - } - }, - .wait_thread => { - // this.poll.wait_thread.poll_ref.deactivate(vm.event_loop_handle.?); - this.poll.wait_thread.poll_ref.deactivate(GlobalHandle.init(this.globalThis).platformEventLoop()); - }, - } + pub fn loop(this: *PipeReader) *uws.Loop { + return this.event_loop.loop(); + } - this.onExit(this.globalThis); - } + pub fn deinit(this: *PipeReader) void { + log("PipeReader(0x{x}, {s}) deinit()", .{ @intFromPtr(this), @tagName(this.out_type) }); + if (comptime Environment.isPosix) { + std.debug.assert(this.reader.isDone() or this.state == .err); } - fn runOnExit(this: *@This(), globalThis: GlobalRef) void { - log("run on exit {d}", .{this.pid}); - _ = globalThis; - const waitpid_error = this.waitpid_err; - _ = waitpid_error; - this.waitpid_err = null; - - // FIXME remove when we get rid of old shell interpreter - if (this.cmd_parent) |cmd| { - if (cmd.exit_code == null) { - // defer this.shell_state = null; - cmd.onExit(this.exit_code.?); - // FIXME handle waitpid_error here like below - } - } + if (comptime Environment.isWindows) { + std.debug.assert(this.reader.source == null or this.reader.source.?.isClosed()); + } - // if (this.on_exit_callback.trySwap()) |callback| { - // const waitpid_value: JSValue = - // if (waitpid_error) |err| - // err.toJSC(globalThis) - // else - // JSC.JSValue.jsUndefined(); - - // const this_value = if (this_jsvalue.isEmptyOrUndefinedOrNull()) JSC.JSValue.jsUndefined() else this_jsvalue; - // this_value.ensureStillAlive(); - - // const args = [_]JSValue{ - // this_value, - // this.getExitCode(globalThis), - // this.getSignalCode(globalThis), - // waitpid_value, - // }; - - // const result = callback.callWithThis( - // globalThis, - // this_value, - // &args, - // ); - - // if (result.isAnyError()) { - // globalThis.bunVM().onUnhandledError(globalThis, result); - // } - // } + if (this.state == .done) { + bun.default_allocator.free(this.state.done); } - fn onExit( - this: *@This(), - globalThis: GlobalRef, - ) void { - log("onExit({d}) = {d}, \"{s}\"", .{ this.pid, if (this.exit_code) |e| @as(i32, @intCast(e)) else -1, if (this.signal_code) |code| @tagName(code) else "" }); - // defer this.updateHasPendingActivity(); - - if (this.hasExited()) { - { - // this.flags.waiting_for_onexit = true; - - // const Holder = struct { - // process: *@This(), - // task: JSC.AnyTask, - - // pub fn unref(self: *@This()) void { - // // this calls disableKeepingProcessAlive on pool_ref and stdin, stdout, stderr - // self.process.flags.waiting_for_onexit = false; - // self.process.unref(true); - // // self.process.updateHasPendingActivity(); - // bun.default_allocator.destroy(self); - // } - // }; - - // var holder = bun.default_allocator.create(Holder) catch @panic("OOM"); - - // holder.* = .{ - // .process = this, - // .task = JSC.AnyTask.New(Holder, Holder.unref).init(holder), - // }; - - // this.globalThis.bunVM().enqueueTask(JSC.Task.init(&holder.task)); - } + this.buffered_output.deinit(); - this.runOnExit(globalThis); - } - } + this.reader.deinit(); + this.destroy(); + } +}; - const os = std.os; +pub const StdioKind = enum { + stdin, + stdout, + stderr, +}; - pub fn extractStdioBlob( - globalThis: *JSC.JSGlobalObject, - blob: JSC.WebCore.AnyBlob, - i: u32, - stdio_array: []Stdio, - ) bool { - return util.extractStdioBlob(globalThis, blob, i, stdio_array); +pub inline fn assertStdioResult(result: StdioResult) void { + if (comptime Environment.allow_assert) { + if (Environment.isPosix) { + if (result) |fd| { + std.debug.assert(fd != bun.invalid_fd); + } } - }; + } } - -const WaiterThread = bun.JSC.Subprocess.WaiterThread; diff --git a/src/shell/util.zig b/src/shell/util.zig index ff15942c55bd30..1b448c818c7646 100644 --- a/src/shell/util.zig +++ b/src/shell/util.zig @@ -25,134 +25,6 @@ pub const OutKind = enum { } }; -pub const Stdio = union(enum) { - /// When set to true, it means to capture the output - inherit: struct { captured: ?*bun.ByteList = null }, - ignore: void, - fd: bun.FileDescriptor, - dup2: struct { out: OutKind, to: OutKind }, - path: JSC.Node.PathLike, - blob: JSC.WebCore.AnyBlob, - pipe: ?JSC.WebCore.ReadableStream, - array_buffer: struct { buf: JSC.ArrayBuffer.Strong, from_jsc: bool = false }, - - pub fn isPiped(self: Stdio) bool { - return switch (self) { - .array_buffer, .blob, .pipe => true, - .inherit => self.inherit.captured != null, - else => false, - }; - } - - pub fn setUpChildIoPosixSpawn( - stdio: @This(), - actions: *PosixSpawn.Actions, - pipe_fd: [2]bun.FileDescriptor, - stderr_pipe_fds: [2]bun.FileDescriptor, - comptime std_fileno: bun.FileDescriptor, - ) !void { - switch (stdio) { - .dup2 => { - // This is a hack to get around the ordering of the spawn actions. - // If stdout is set so that it redirects to stderr, the order of actions will be like this: - // 0. dup2(stderr, stdout) - this makes stdout point to stderr - // 1. setup stderr (will make stderr point to write end of `stderr_pipe_fds`) - // This is actually wrong, 0 will execute before 1 so stdout ends up writing to stderr instead of the pipe - // So we have to instead do `dup2(stderr_pipe_fd[1], stdout)` - // Right now we only allow one output redirection so it's okay. - if (comptime std_fileno == bun.STDOUT_FD) { - const idx: usize = if (std_fileno == bun.STDIN_FD) 0 else 1; - try actions.dup2(stderr_pipe_fds[idx], stdio.dup2.out.toFd()); - } else try actions.dup2(stdio.dup2.to.toFd(), stdio.dup2.out.toFd()); - }, - .array_buffer, .blob, .pipe => { - std.debug.assert(!(stdio == .blob and stdio.blob.needsToReadFile())); - const idx: usize = if (std_fileno == bun.STDIN_FD) 0 else 1; - - try actions.dup2(pipe_fd[idx], std_fileno); - try actions.close(pipe_fd[1 - idx]); - }, - .inherit => { - if (stdio.inherit.captured != null) { - // Same as above - std.debug.assert(!(stdio == .blob and stdio.blob.needsToReadFile())); - const idx: usize = if (std_fileno == bun.STDIN_FD) 0 else 1; - - try actions.dup2(pipe_fd[idx], std_fileno); - try actions.close(pipe_fd[1 - idx]); - return; - } - - if (comptime Environment.isMac) { - try actions.inherit(std_fileno); - } else { - try actions.dup2(std_fileno, std_fileno); - } - }, - .fd => |fd| { - try actions.dup2(fd, std_fileno); - }, - .path => |pathlike| { - const flag = if (std_fileno == bun.STDIN_FD) @as(u32, os.O.RDONLY) else @as(u32, std.os.O.WRONLY); - try actions.open(std_fileno, pathlike.slice(), flag | std.os.O.CREAT, 0o664); - }, - .ignore => { - const flag = if (std_fileno == bun.STDIN_FD) @as(u32, os.O.RDONLY) else @as(u32, std.os.O.WRONLY); - try actions.openZ(std_fileno, "/dev/null", flag, 0o664); - }, - } - } -}; - -pub fn extractStdioBlob( - globalThis: *JSC.JSGlobalObject, - blob: JSC.WebCore.AnyBlob, - i: u32, - stdio_array: []Stdio, -) bool { - const fd = bun.stdio(i); - - if (blob.needsToReadFile()) { - if (blob.store()) |store| { - if (store.data.file.pathlike == .fd) { - if (store.data.file.pathlike.fd == fd) { - stdio_array[i] = Stdio{ .inherit = .{} }; - } else { - switch (bun.FDTag.get(i)) { - .stdin => { - if (i == 1 or i == 2) { - globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); - return false; - } - }, - - .stdout, .stderr => { - if (i == 0) { - globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); - return false; - } - }, - else => {}, - } - - stdio_array[i] = Stdio{ .fd = store.data.file.pathlike.fd }; - } - - return true; - } - - stdio_array[i] = .{ .path = store.data.file.pathlike.path }; - return true; - } - } - - if (i == 1 or i == 2) { - globalThis.throwInvalidArguments("Blobs are immutable, and cannot be used for stdout/stderr", .{}); - return false; - } - - stdio_array[i] = .{ .blob = blob }; - return true; -} +pub const Stdio = bun.spawn.Stdio; pub const WatchFd = if (Environment.isLinux) std.os.fd_t else i32; diff --git a/src/string_mutable.zig b/src/string_mutable.zig index cdf89173caee2d..42bfdd4fa11538 100644 --- a/src/string_mutable.zig +++ b/src/string_mutable.zig @@ -37,7 +37,7 @@ pub const MutableString = struct { } pub fn owns(this: *const MutableString, slice: []const u8) bool { - return bun.isSliceInBuffer(u8, slice, this.list.items.ptr[0..this.list.capacity]); + return bun.isSliceInBuffer(slice, this.list.items.ptr[0..this.list.capacity]); } pub fn growIfNeeded(self: *MutableString, amount: usize) !void { diff --git a/src/sys.zig b/src/sys.zig index c729b61724052d..a9b441dc8a7109 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -64,6 +64,7 @@ else pub const Tag = enum(u8) { TODO, + dup, access, chmod, @@ -116,7 +117,7 @@ pub const Tag = enum(u8) { truncate, realpath, futime, - listen, + pidfd_open, kevent, kqueue, @@ -130,10 +131,17 @@ pub const Tag = enum(u8) { readv, preadv, ioctl_ficlone, + accept, + bind2, + connect2, + listen, + pipe, + try_write, uv_spawn, uv_pipe, - pipe, + + // Below this line are Windows API calls only. WriteFile, NtQueryDirectoryFile, @@ -156,7 +164,7 @@ pub const Error = struct { const retry_errno = if (Environment.isLinux) @as(Int, @intCast(@intFromEnum(E.AGAIN))) else if (Environment.isMac) - @as(Int, @intCast(@intFromEnum(E.WOULDBLOCK))) + @as(Int, @intCast(@intFromEnum(E.AGAIN))) else @as(Int, @intCast(@intFromEnum(E.INTR))); @@ -207,7 +215,7 @@ pub const Error = struct { pub const retry = Error{ .errno = retry_errno, - .syscall = .retry, + .syscall = .read, }; pub inline fn withFd(this: Error, fd: anytype) Error { @@ -237,6 +245,30 @@ pub const Error = struct { }; } + pub fn name(this: *const Error) []const u8 { + if (comptime Environment.isWindows) { + const system_errno = brk: { + // setRuntimeSafety(false) because we use tagName function, which will be null on invalid enum value. + @setRuntimeSafety(false); + if (this.from_libuv) { + break :brk @as(C.SystemErrno, @enumFromInt(@intFromEnum(bun.windows.libuv.translateUVErrorToE(this.errno)))); + } + + break :brk @as(C.SystemErrno, @enumFromInt(this.errno)); + }; + if (std.enums.tagName(bun.C.SystemErrno, system_errno)) |errname| { + return errname; + } + } else if (this.errno > 0 and this.errno < C.SystemErrno.max) { + const system_errno = @as(C.SystemErrno, @enumFromInt(this.errno)); + if (std.enums.tagName(bun.C.SystemErrno, system_errno)) |errname| { + return errname; + } + } + + return "UNKNOWN"; + } + pub fn toSystemError(this: Error) SystemError { var err = SystemError{ .errno = @as(c_int, this.errno) * -1, @@ -952,7 +984,7 @@ pub fn openatA(dirfd: bun.FileDescriptor, file_path: []const u8, flags: bun.Mode const pathZ = std.os.toPosixPath(file_path) catch return Maybe(bun.FileDescriptor){ .err = .{ - .errno = @intFromEnum(bun.C.E.NOMEM), + .errno = @intFromEnum(bun.C.E.NAMETOOLONG), .syscall = .open, }, }; @@ -1005,11 +1037,20 @@ pub const max_count = switch (builtin.os.tag) { pub fn write(fd: bun.FileDescriptor, bytes: []const u8) Maybe(usize) { const adjusted_len = @min(max_count, bytes.len); + var debug_timer = bun.Output.DebugTimer.start(); + + defer { + if (comptime Environment.isDebug) { + if (debug_timer.timer.read() > std.time.ns_per_ms) { + bun.Output.debugWarn("write({}, {d}) blocked for {}", .{ fd, bytes.len, debug_timer }); + } + } + } return switch (Environment.os) { .mac => { const rc = system.@"write$NOCANCEL"(fd.cast(), bytes.ptr, adjusted_len); - log("write({}, {d}) = {d}", .{ fd, adjusted_len, rc }); + log("write({}, {d}) = {d} ({})", .{ fd, adjusted_len, rc, debug_timer }); if (Maybe(usize).errnoSysFd(rc, .write, fd)) |err| { return err; @@ -1020,7 +1061,7 @@ pub fn write(fd: bun.FileDescriptor, bytes: []const u8) Maybe(usize) { .linux => { while (true) { const rc = sys.write(fd.cast(), bytes.ptr, adjusted_len); - log("write({}, {d}) = {d}", .{ fd, adjusted_len, rc }); + log("write({}, {d}) = {d} {}", .{ fd, adjusted_len, rc, debug_timer }); if (Maybe(usize).errnoSysFd(rc, .write, fd)) |err| { if (err.getErrno() == .INTR) continue; @@ -1041,8 +1082,8 @@ pub fn write(fd: bun.FileDescriptor, bytes: []const u8) Maybe(usize) { &bytes_written, null, ); - log("WriteFile({}, {d}) = {d} (written: {d})", .{ fd, adjusted_len, rc, bytes_written }); if (rc == 0) { + log("WriteFile({}, {d}) = {s}", .{ fd, adjusted_len, @tagName(bun.windows.getLastErrno()) }); return .{ .err = Syscall.Error{ .errno = @intFromEnum(bun.windows.getLastErrno()), @@ -1051,6 +1092,9 @@ pub fn write(fd: bun.FileDescriptor, bytes: []const u8) Maybe(usize) { }, }; } + + log("WriteFile({}, {d}) = {d}", .{ fd, adjusted_len, bytes_written }); + return Maybe(usize){ .result = bytes_written }; }, else => @compileError("Not implemented yet"), @@ -1307,13 +1351,43 @@ pub fn read(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } }, - .windows => sys_uv.read(fd, buf), + .windows => if (bun.FDImpl.decode(fd).kind == .uv) + sys_uv.read(fd, buf) + else { + var amount_read: u32 = 0; + const rc = kernel32.ReadFile(fd.cast(), buf.ptr, @as(u32, @intCast(adjusted_len)), &amount_read, null); + if (rc == windows.FALSE) { + const ret = .{ + .err = Syscall.Error{ + .errno = @intFromEnum(bun.windows.getLastErrno()), + .syscall = .read, + .fd = fd, + }, + }; + + if (comptime Environment.isDebug) { + log("ReadFile({}, {d}) = {s} ({})", .{ fd, adjusted_len, ret.err.name(), debug_timer }); + } + + return ret; + } + log("ReadFile({}, {d}) = {d} ({})", .{ fd, adjusted_len, amount_read, debug_timer }); + + return Maybe(usize){ .result = amount_read }; + }, else => @compileError("read is not implemented on this platform"), }; } +const socket_flags_nonblock = bun.C.MSG_DONTWAIT | bun.C.MSG_NOSIGNAL; + +pub fn recvNonBlock(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { + return recv(fd, buf, socket_flags_nonblock); +} + pub fn recv(fd: bun.FileDescriptor, buf: []u8, flag: u32) Maybe(usize) { const adjusted_len = @min(buf.len, max_count); + const debug_timer = bun.Output.DebugTimer.start(); if (comptime Environment.allow_assert) { if (adjusted_len == 0) { bun.Output.debugWarn("recv() called with 0 length buffer", .{}); @@ -1322,43 +1396,57 @@ pub fn recv(fd: bun.FileDescriptor, buf: []u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { const rc = system.@"recvfrom$NOCANCEL"(fd.cast(), buf.ptr, adjusted_len, flag, null, null); - log("recv({}, {d}, {d}) = {d}", .{ fd, adjusted_len, flag, rc }); if (Maybe(usize).errnoSys(rc, .recv)) |err| { + log("recv({}, {d}) = {s} {}", .{ fd, adjusted_len, err.err.name(), debug_timer }); return err; } + log("recv({}, {d}) = {d} {}", .{ fd, adjusted_len, rc, debug_timer }); + return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } else { while (true) { - const rc = linux.recvfrom(fd.cast(), buf.ptr, adjusted_len, flag | os.SOCK.CLOEXEC | linux.MSG.CMSG_CLOEXEC, null, null); - log("recv({}, {d}, {d}) = {d}", .{ fd, adjusted_len, flag, rc }); + const rc = linux.recvfrom(fd.cast(), buf.ptr, adjusted_len, flag, null, null); if (Maybe(usize).errnoSysFd(rc, .recv, fd)) |err| { if (err.getErrno() == .INTR) continue; + log("recv({}, {d}) = {s} {}", .{ fd, adjusted_len, err.err.name(), debug_timer }); return err; } + log("recv({}, {d}) = {d} {}", .{ fd, adjusted_len, rc, debug_timer }); return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } } } +pub fn sendNonBlock(fd: bun.FileDescriptor, buf: []const u8) Maybe(usize) { + return send(fd, buf, socket_flags_nonblock); +} + pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) { if (comptime Environment.isMac) { - const rc = system.@"sendto$NOCANCEL"(fd, buf.ptr, buf.len, flag, null, 0); + const rc = system.@"sendto$NOCANCEL"(fd.cast(), buf.ptr, buf.len, flag, null, 0); + if (Maybe(usize).errnoSys(rc, .send)) |err| { + syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; } + + syslog("send({}, {d}) = {d}", .{ fd, buf.len, rc }); + return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } else { while (true) { - const rc = linux.sendto(fd, buf.ptr, buf.len, flag | os.SOCK.CLOEXEC | os.MSG.NOSIGNAL, null, 0); + const rc = linux.sendto(fd.cast(), buf.ptr, buf.len, flag, null, 0); if (Maybe(usize).errnoSys(rc, .send)) |err| { if (err.getErrno() == .INTR) continue; + syslog("send({}, {d}) = {s}", .{ fd, buf.len, err.err.name() }); return err; } + syslog("send({}, {d}) = {d}", .{ fd, buf.len, rc }); return Maybe(usize){ .result = @as(usize, @intCast(rc)) }; } } @@ -1503,10 +1591,20 @@ pub fn fcopyfile(fd_in: bun.FileDescriptor, fd_out: bun.FileDescriptor, flags: u pub fn unlink(from: [:0]const u8) Maybe(void) { while (true) { - if (Maybe(void).errnoSys(sys.unlink(from), .unlink)) |err| { - if (err.getErrno() == .INTR) continue; - return err; + if (bun.Environment.isWindows) { + if (sys.unlink(from) != 0) { + const last_error = kernel32.GetLastError(); + const errno = Syscall.getErrno(@as(u16, @intFromEnum(last_error))); + if (errno == .INTR) continue; + return .{ .err = Syscall.Error.fromCode(errno, .unlink) }; + } + } else { + if (Maybe(void).errnoSys(sys.unlink(from), .unlink)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } } + log("unlink({s}) = 0", .{from}); return Maybe(void).success; } } @@ -1582,9 +1680,8 @@ pub fn getFdPath(fd: bun.FileDescriptor, out_buffer: *[MAX_PATH_BYTES]u8) Maybe( }, .linux => { // TODO: alpine linux may not have /proc/self - var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; - const proc_path = std.fmt.bufPrintZ(procfs_buf[0..], "/proc/self/fd/{d}\x00", .{fd.cast()}) catch unreachable; - + var procfs_buf: ["/proc/self/fd/-2147483648".len + 1:0]u8 = undefined; + 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] }, @@ -1919,10 +2016,11 @@ pub fn pipe() Maybe([2]bun.FileDescriptor) { )) |err| { return err; } + log("pipe() = [{d}, {d}]", .{ fds[0], fds[1] }); return .{ .result = .{ bun.toFD(fds[0]), bun.toFD(fds[1]) } }; } -pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { +pub fn dupWithFlags(fd: bun.FileDescriptor, flags: i32) Maybe(bun.FileDescriptor) { if (comptime Environment.isWindows) { var target: windows.HANDLE = undefined; const process = kernel32.GetCurrentProcess(); @@ -1935,6 +2033,7 @@ pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { w.TRUE, w.DUPLICATE_SAME_ACCESS, ); + log("dup({d}) = {d}", .{ fd.cast(), out }); if (out == 0) { if (Maybe(bun.FileDescriptor).errnoSysFd(0, .dup, fd)) |err| { return err; @@ -1943,9 +2042,23 @@ pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { return Maybe(bun.FileDescriptor){ .result = bun.toFD(target) }; } - const out = std.c.dup(fd.cast()); - log("dup({}) = {d}", .{ fd, out }); - return Maybe(bun.FileDescriptor).errnoSysFd(out, .dup, fd) orelse Maybe(bun.FileDescriptor){ .result = bun.toFD(out) }; + const ArgType = if (comptime Environment.isLinux) usize else c_int; + const out = system.fcntl(fd.cast(), @as(i32, bun.C.F.DUPFD_CLOEXEC), @as(ArgType, 0)); + log("dup({d}) = {d}", .{ fd.cast(), out }); + if (Maybe(bun.FileDescriptor).errnoSysFd(out, .dup, fd)) |err| { + return err; + } + + if (flags != 0) { + const fd_flags: ArgType = @intCast(system.fcntl(@intCast(out), @as(i32, std.os.F.GETFD), @as(ArgType, 0))); + _ = system.fcntl(@intCast(out), @as(i32, std.os.F.SETFD), @as(ArgType, @intCast(fd_flags | @as(ArgType, @intCast(flags))))); + } + + return Maybe(bun.FileDescriptor){ .result = bun.toFD(out) }; +} + +pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { + return dupWithFlags(fd, 0); } pub fn linkat(dir_fd: bun.FileDescriptor, basename: []const u8, dest_dir_fd: bun.FileDescriptor, dest_name: []const u8) Maybe(void) { @@ -1992,3 +2105,213 @@ pub fn linkatTmpfile(tmpfd: bun.FileDescriptor, dirfd: bun.FileDescriptor, name: name, ) orelse Maybe(void).success; } + +/// On Linux, this `preadv2(2)` to attempt to read a blocking file descriptor without blocking. +/// +/// On other platforms, this is just a wrapper around `read(2)`. +pub fn readNonblocking(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { + if (Environment.isLinux) { + while (bun.C.linux.RWFFlagSupport.isMaybeSupported()) { + const iovec = [1]std.os.iovec{.{ + .iov_base = buf.ptr, + .iov_len = buf.len, + }}; + var debug_timer = bun.Output.DebugTimer.start(); + + // Note that there is a bug on Linux Kernel 5 + const rc = C.sys_preadv2(@intCast(fd.int()), &iovec, 1, -1, linux.RWF.NOWAIT); + + if (comptime Environment.isDebug) { + log("preadv2({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer }); + + if (debug_timer.timer.read() > std.time.ns_per_ms) { + bun.Output.debugWarn("preadv2({}, {d}) blocked for {}", .{ fd, buf.len, debug_timer }); + } + } + + if (Maybe(usize).errnoSysFd(rc, .read, fd)) |err| { + switch (err.getErrno()) { + .OPNOTSUPP, .NOSYS => { + bun.C.linux.RWFFlagSupport.disable(); + switch (bun.isReadable(fd)) { + .hup, .ready => return read(fd, buf), + else => return .{ .err = Error.retry }, + } + }, + .INTR => continue, + else => return err, + } + } + + return .{ .result = @as(usize, @intCast(rc)) }; + } + } + + return read(fd, buf); +} + +/// On Linux, this `pwritev(2)` to attempt to read a blocking file descriptor without blocking. +/// +/// On other platforms, this is just a wrapper around `read(2)`. +pub fn writeNonblocking(fd: bun.FileDescriptor, buf: []const u8) Maybe(usize) { + if (Environment.isLinux) { + while (bun.C.linux.RWFFlagSupport.isMaybeSupported()) { + const iovec = [1]std.os.iovec_const{.{ + .iov_base = buf.ptr, + .iov_len = buf.len, + }}; + + var debug_timer = bun.Output.DebugTimer.start(); + + const rc = C.sys_pwritev2(@intCast(fd.int()), &iovec, 1, -1, linux.RWF.NOWAIT); + + if (comptime Environment.isDebug) { + log("pwritev2({}, {d}) = {d} ({})", .{ fd, buf.len, rc, debug_timer }); + + if (debug_timer.timer.read() > std.time.ns_per_ms) { + bun.Output.debugWarn("pwritev2({}, {d}) blocked for {}", .{ fd, buf.len, debug_timer }); + } + } + + if (Maybe(usize).errnoSysFd(rc, .write, fd)) |err| { + switch (err.getErrno()) { + .OPNOTSUPP, .NOSYS => { + bun.C.linux.RWFFlagSupport.disable(); + switch (bun.isWritable(fd)) { + .hup, .ready => return write(fd, buf), + else => return .{ .err = Error.retry }, + } + }, + .INTR => continue, + else => return err, + } + } + + return .{ .result = @as(usize, @intCast(rc)) }; + } + } + + return write(fd, buf); +} + +pub fn isPollable(mode: mode_t) bool { + return os.S.ISFIFO(mode) or os.S.ISSOCK(mode); +} + +const This = @This(); + +pub const File = struct { + // "handle" matches std.fs.File + handle: bun.FileDescriptor, + + pub fn from(other: anytype) File { + const T = @TypeOf(other); + + if (T == File) { + return other; + } + + if (T == std.os.fd_t) { + return File{ .handle = bun.toFD(other) }; + } + + if (T == bun.FileDescriptor) { + return File{ .handle = other }; + } + + if (T == std.fs.File) { + return File{ .handle = bun.toFD(other.handle) }; + } + + if (T == std.fs.Dir) { + return File{ .handle = bun.toFD(other.handle) }; + } + + if (comptime Environment.isWindows) { + if (T == bun.windows.HANDLE) { + return File{ .handle = bun.toFD(other) }; + } + } + + if (comptime Environment.isLinux) { + if (T == u64) { + return File{ .handle = bun.toFD(other) }; + } + } + + @compileError("Unsupported type " ++ bun.meta.typeName(T)); + } + + pub fn write(self: File, buf: []const u8) Maybe(usize) { + return This.write(self.handle, buf); + } + + pub fn read(self: File, buf: []u8) Maybe(usize) { + return This.read(self.handle, buf); + } + + pub fn writeAll(self: File, buf: []const u8) Maybe(void) { + var remain = buf; + while (remain.len > 0) { + const rc = This.write(self.handle, remain); + switch (rc) { + .err => |err| return .{ .err = err }, + .result => |amt| { + if (amt == 0) { + return .{ .result = {} }; + } + remain = remain[amt..]; + }, + } + } + + return .{ .result = {} }; + } + + pub const ReadError = anyerror; + + fn stdIoRead(this: File, buf: []u8) ReadError!usize { + return try this.read(buf).unwrap(); + } + + pub const Reader = std.io.Reader(File, anyerror, stdIoRead); + + pub fn reader(self: File) Reader { + return Reader{ .context = self }; + } + + pub const WriteError = anyerror; + fn stdIoWrite(this: File, bytes: []const u8) WriteError!usize { + try this.writeAll(bytes).unwrap(); + + return bytes.len; + } + + fn stdIoWriteQuietDebug(this: File, bytes: []const u8) WriteError!usize { + bun.Output.disableScopedDebugWriter(); + defer bun.Output.enableScopedDebugWriter(); + try this.writeAll(bytes).unwrap(); + + return bytes.len; + } + + pub const Writer = std.io.Writer(File, anyerror, stdIoWrite); + pub const QuietWriter = if (Environment.isDebug) std.io.Writer(File, anyerror, stdIoWriteQuietDebug) else Writer; + + pub fn writer(self: File) Writer { + return Writer{ .context = self }; + } + + pub fn quietWriter(self: File) QuietWriter { + return QuietWriter{ .context = self }; + } + + pub fn isTty(self: File) bool { + return std.os.isatty(self.handle.cast()); + } + + pub fn close(self: File) void { + // TODO: probably return the error? we have a lot of code paths which do not so we are keeping for now + _ = This.close(self.handle); + } +}; diff --git a/src/sys_uv.zig b/src/sys_uv.zig index e4c3767b7ccfcc..4b50b41a33ec79 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -54,9 +54,9 @@ pub fn open(file_path: [:0]const u8, c_flags: bun.Mode, _perm: bun.Mode) Maybe(b const rc = uv.uv_fs_open(uv.Loop.get(), &req, file_path.ptr, flags, perm, null); log("uv open({s}, {d}, {d}) = {d}", .{ file_path, flags, perm, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .open, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .open } } else - .{ .result = bun.toFD(@as(i32, @intCast(req.result.value))) }; + .{ .result = bun.toFD(@as(i32, @intCast(req.result.int()))) }; } pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { @@ -67,7 +67,7 @@ pub fn mkdir(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv mkdir({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .mkdir, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .mkdir } } else .{ .result = {} }; } @@ -81,7 +81,7 @@ pub fn chmod(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv chmod({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .chmod, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .chmod } } else .{ .result = {} }; } @@ -94,7 +94,7 @@ pub fn fchmod(fd: FileDescriptor, flags: bun.Mode) Maybe(void) { log("uv fchmod({}, {d}) = {d}", .{ uv_fd, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fchmod, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .fchmod } } else .{ .result = {} }; } @@ -107,7 +107,7 @@ pub fn chown(file_path: [:0]const u8, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe( log("uv chown({s}, {d}, {d}) = {d}", .{ file_path, uid, gid, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .chown, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .chown } } else .{ .result = {} }; } @@ -121,7 +121,7 @@ pub fn fchown(fd: FileDescriptor, uid: uv.uv_uid_t, gid: uv.uv_uid_t) Maybe(void log("uv chown({}, {d}, {d}) = {d}", .{ uv_fd, uid, gid, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fchown, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .fchown } } else .{ .result = {} }; } @@ -134,7 +134,7 @@ pub fn access(file_path: [:0]const u8, flags: bun.Mode) Maybe(void) { log("uv access({s}, {d}) = {d}", .{ file_path, flags, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .access, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .access } } else .{ .result = {} }; } @@ -147,7 +147,7 @@ pub fn rmdir(file_path: [:0]const u8) Maybe(void) { log("uv rmdir({s}) = {d}", .{ file_path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .rmdir, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .rmdir } } else .{ .result = {} }; } @@ -160,7 +160,7 @@ pub fn unlink(file_path: [:0]const u8) Maybe(void) { log("uv unlink({s}) = {d}", .{ file_path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .unlink, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .unlink } } else .{ .result = {} }; } @@ -174,14 +174,14 @@ pub fn readlink(file_path: [:0]const u8, buf: []u8) Maybe(usize) { if (rc.errno()) |errno| { log("uv readlink({s}) = {d}, [err]", .{ file_path, rc.int() }); - return .{ .err = .{ .errno = errno, .syscall = .readlink, .from_libuv = true } }; + return .{ .err = .{ .errno = errno, .syscall = .readlink } }; } else { // Seems like `rc` does not contain the errno? std.debug.assert(rc.int() == 0); const slice = bun.span(req.ptrAs([*:0]u8)); if (slice.len > buf.len) { log("uv readlink({s}) = {d}, {s} TRUNCATED", .{ file_path, rc.int(), slice }); - return .{ .err = .{ .errno = @intFromEnum(E.NOMEM), .syscall = .readlink, .from_libuv = true } }; + return .{ .err = .{ .errno = @intFromEnum(E.NOMEM), .syscall = .readlink } }; } log("uv readlink({s}) = {d}, {s}", .{ file_path, rc.int(), slice }); @memcpy(buf[0..slice.len], slice); @@ -198,7 +198,7 @@ pub fn rename(from: [:0]const u8, to: [:0]const u8) Maybe(void) { log("uv rename({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .rename, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .rename } } else .{ .result = {} }; } @@ -212,7 +212,7 @@ pub fn link(from: [:0]const u8, to: [:0]const u8) Maybe(void) { log("uv link({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .link, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .link } } else .{ .result = {} }; } @@ -226,7 +226,7 @@ pub fn symlinkUV(from: [:0]const u8, to: [:0]const u8, flags: c_int) Maybe(void) log("uv symlink({s}, {s}) = {d}", .{ from, to, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .symlink, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .symlink } } else .{ .result = {} }; } @@ -239,7 +239,7 @@ pub fn ftruncate(fd: FileDescriptor, size: isize) Maybe(void) { log("uv ftruncate({}, {d}) = {d}", .{ uv_fd, size, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .ftruncate, .fd = fd, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .ftruncate, .fd = fd } } else .{ .result = {} }; } @@ -252,7 +252,7 @@ pub fn fstat(fd: FileDescriptor) Maybe(bun.Stat) { log("uv fstat({}) = {d}", .{ uv_fd, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fstat, .fd = fd, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .fstat, .fd = fd } } else .{ .result = req.statbuf }; } @@ -265,7 +265,7 @@ pub fn fdatasync(fd: FileDescriptor) Maybe(void) { log("uv fdatasync({}) = {d}", .{ uv_fd, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fdatasync, .fd = fd, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .fdatasync, .fd = fd } } else .{ .result = {} }; } @@ -278,7 +278,7 @@ pub fn fsync(fd: FileDescriptor) Maybe(void) { log("uv fsync({d}) = {d}", .{ uv_fd, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .fsync, .fd = fd, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .fsync, .fd = fd } } else .{ .result = {} }; } @@ -291,7 +291,7 @@ pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { log("uv stat({s}) = {d}", .{ path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .stat, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .stat } } else .{ .result = req.statbuf }; } @@ -304,7 +304,7 @@ pub fn lstat(path: [:0]const u8) Maybe(bun.Stat) { log("uv lstat({s}) = {d}", .{ path, rc.int() }); return if (rc.errno()) |errno| - .{ .err = .{ .errno = errno, .syscall = .lstat, .from_libuv = true } } + .{ .err = .{ .errno = errno, .syscall = .lstat } } else .{ .result = req.statbuf }; } @@ -345,7 +345,7 @@ pub fn preadv(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64 } if (rc.errno()) |errno| { - return .{ .err = .{ .errno = errno, .fd = fd, .syscall = .read, .from_libuv = true } }; + return .{ .err = .{ .errno = errno, .fd = fd, .syscall = .read } }; } else { return .{ .result = @as(usize, @intCast(rc.int())) }; } @@ -379,7 +379,7 @@ pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVecConst, positio } if (rc.errno()) |errno| { - return .{ .err = .{ .errno = errno, .fd = fd, .syscall = .write, .from_libuv = true } }; + return .{ .err = .{ .errno = errno, .fd = fd, .syscall = .write } }; } else { return .{ .result = @as(usize, @intCast(rc.int())) }; } diff --git a/src/tagged_pointer.zig b/src/tagged_pointer.zig index 3ec0456e57a5d3..d18207b4f6ebb2 100644 --- a/src/tagged_pointer.zig +++ b/src/tagged_pointer.zig @@ -54,12 +54,13 @@ pub const TaggedPointer = packed struct { } }; +const TypeMapT = struct { + value: TagSize, + ty: type, + name: []const u8, +}; pub fn TypeMap(comptime Types: anytype) type { - return [Types.len]struct { - value: TagSize, - ty: type, - name: []const u8, - }; + return [Types.len]TypeMapT; } pub fn TagTypeEnumWithTypeMap(comptime Types: anytype) struct { @@ -68,7 +69,9 @@ pub fn TagTypeEnumWithTypeMap(comptime Types: anytype) struct { } { var typeMap: TypeMap(Types) = undefined; var enumFields: [Types.len]std.builtin.Type.EnumField = undefined; - var decls = [_]std.builtin.Type.Declaration{}; + + @memset(&enumFields, std.mem.zeroes(std.builtin.Type.EnumField)); + @memset(&typeMap, TypeMapT{ .value = 0, .ty = void, .name = "" }); inline for (Types, 0..) |field, i| { const name = comptime typeBaseName(@typeName(field)); @@ -84,7 +87,7 @@ pub fn TagTypeEnumWithTypeMap(comptime Types: anytype) struct { .Enum = .{ .tag_type = TagSize, .fields = &enumFields, - .decls = &decls, + .decls = &.{}, .is_exhaustive = false, }, }), @@ -105,6 +108,10 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { pub const Null = .{ .repr = .{ ._ptr = 0, .data = 0 } }; + pub fn clear(this: *@This()) void { + this.* = Null; + } + pub fn typeFromTag(comptime the_tag: comptime_int) type { for (type_map) |entry| { if (entry.value == the_tag) return entry.ty; @@ -180,11 +187,16 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { } pub inline fn init(_ptr: anytype) @This() { + const tyinfo = @typeInfo(@TypeOf(_ptr)); + if (tyinfo != .Pointer) @compileError("Only pass pointers to TaggedPointerUnion.init(), you gave us a: " ++ @typeName(@TypeOf(_ptr))); + const Type = std.meta.Child(@TypeOf(_ptr)); return initWithType(Type, _ptr); } pub inline fn initWithType(comptime Type: type, _ptr: anytype) @This() { + const tyinfo = @typeInfo(@TypeOf(_ptr)); + if (tyinfo != .Pointer) @compileError("Only pass pointers to TaggedPointerUnion.init(), you gave us a: " ++ @typeName(@TypeOf(_ptr))); const name = comptime typeBaseName(@typeName(Type)); // there will be a compiler error if the passed in type doesn't exist in the enum @@ -194,6 +206,27 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type { pub inline fn isNull(this: This) bool { return this.repr._ptr == 0; } + + pub inline fn call(this: This, comptime fn_name: []const u8, args_without_this: anytype, comptime Ret: type) Ret { + inline for (type_map) |entry| { + if (this.repr.data == entry.value) { + const pointer = this.as(entry.ty); + const func = &@field(entry.ty, fn_name); + const args = brk: { + var args: std.meta.ArgsTuple(@TypeOf(@field(entry.ty, fn_name))) = undefined; + args[0] = pointer; + + inline for (args_without_this, 1..) |a, i| { + args[i] = a; + } + + break :brk args; + }; + return @call(.auto, func, args); + } + } + @panic("Invalid tag"); + } }; } diff --git a/test/bundler/__snapshots__/bun-build-api.test.ts.snap b/test/bundler/__snapshots__/bun-build-api.test.ts.snap index 08a1b40f3d40cb..30746b67d61c01 100644 --- a/test/bundler/__snapshots__/bun-build-api.test.ts.snap +++ b/test/bundler/__snapshots__/bun-build-api.test.ts.snap @@ -1,117 +1,117 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`Bun.build BuildArtifact properties: hash 1`] = `"e4885a8bc2de343a"`; - -exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"cb8abf3391c2971f"`; - -exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"e4885a8bc2de343a"`; - -exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"0000000000000000"`; - -exports[`Bun.build Bun.write(BuildArtifact) 1`] = ` -"var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { - get: all[name], - enumerable: true, - configurable: true, - set: (newValue) => all[name] = () => newValue - }); -}; -var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); - -// test/bundler/fixtures/trivial/fn.js -var exports_fn = {}; -__export(exports_fn, { - fn: () => { - { - return fn; - } - } -}); -function fn(a) { - return a + 42; -} -var init_fn = __esm(() => { -}); - -// test/bundler/fixtures/trivial/index.js -var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); -NS.then(({ fn: fn2 }) => { - console.log(fn2(42)); -}); -" -`; - -exports[`Bun.build outdir + reading out blobs works 1`] = ` -"var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { - get: all[name], - enumerable: true, - configurable: true, - set: (newValue) => all[name] = () => newValue - }); -}; -var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); - -// test/bundler/fixtures/trivial/fn.js -var exports_fn = {}; -__export(exports_fn, { - fn: () => { - { - return fn; - } - } -}); -function fn(a) { - return a + 42; -} -var init_fn = __esm(() => { -}); - -// test/bundler/fixtures/trivial/index.js -var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); -NS.then(({ fn: fn2 }) => { - console.log(fn2(42)); -}); -" -`; - -exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = ` -"var __defProp = Object.defineProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { - get: all[name], - enumerable: true, - configurable: true, - set: (newValue) => all[name] = () => newValue - }); -}; -var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); - -// test/bundler/fixtures/trivial/fn.js -var exports_fn = {}; -__export(exports_fn, { - fn: () => { - { - return fn; - } - } -}); -function fn(a) { - return a + 42; -} -var init_fn = __esm(() => { -}); - -// test/bundler/fixtures/trivial/index.js -var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); -NS.then(({ fn: fn2 }) => { - console.log(fn2(42)); -}); -" -`; +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`Bun.build BuildArtifact properties: hash 1`] = `"e4885a8bc2de343a"`; + +exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"cb8abf3391c2971f"`; + +exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"e4885a8bc2de343a"`; + +exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"0000000000000000"`; + +exports[`Bun.build Bun.write(BuildArtifact) 1`] = ` +"var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { + get: all[name], + enumerable: true, + configurable: true, + set: (newValue) => all[name] = () => newValue + }); +}; +var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); + +// test/bundler/fixtures/trivial/fn.js +var exports_fn = {}; +__export(exports_fn, { + fn: () => { + { + return fn; + } + } +}); +function fn(a) { + return a + 42; +} +var init_fn = __esm(() => { +}); + +// test/bundler/fixtures/trivial/index.js +var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); +NS.then(({ fn: fn2 }) => { + console.log(fn2(42)); +}); +" +`; + +exports[`Bun.build outdir + reading out blobs works 1`] = ` +"var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { + get: all[name], + enumerable: true, + configurable: true, + set: (newValue) => all[name] = () => newValue + }); +}; +var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); + +// test/bundler/fixtures/trivial/fn.js +var exports_fn = {}; +__export(exports_fn, { + fn: () => { + { + return fn; + } + } +}); +function fn(a) { + return a + 42; +} +var init_fn = __esm(() => { +}); + +// test/bundler/fixtures/trivial/index.js +var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); +NS.then(({ fn: fn2 }) => { + console.log(fn2(42)); +}); +" +`; + +exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = ` +"var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { + get: all[name], + enumerable: true, + configurable: true, + set: (newValue) => all[name] = () => newValue + }); +}; +var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); + +// test/bundler/fixtures/trivial/fn.js +var exports_fn = {}; +__export(exports_fn, { + fn: () => { + { + return fn; + } + } +}); +function fn(a) { + return a + 42; +} +var init_fn = __esm(() => { +}); + +// test/bundler/fixtures/trivial/index.js +var NS = Promise.resolve().then(() => (init_fn(), exports_fn)); +NS.then(({ fn: fn2 }) => { + console.log(fn2(42)); +}); +" +`; diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts index aafbda65d18d34..a8d45ed673fe06 100644 --- a/test/cli/install/bun-add.test.ts +++ b/test/cli/install/bun-add.test.ts @@ -62,7 +62,7 @@ it("should add existing package", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -109,7 +109,7 @@ it("should reject missing package", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -152,7 +152,7 @@ it("should reject invalid path without segfault", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -197,7 +197,7 @@ it("should handle semver-like names", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "1.2.3"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -240,7 +240,7 @@ it("should handle @scoped names", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "@bar/baz"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -274,7 +274,7 @@ it("should add dependency with capital letters", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "BaR"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -329,7 +329,7 @@ it("should add exact version with --exact", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "--exact", "BaR"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -385,7 +385,7 @@ it("should add exact version with install.exact", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "BaR"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -440,7 +440,7 @@ it("should add exact version with -E", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "-E", "BaR"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -502,7 +502,7 @@ it("should add dependency with specified semver", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "baz@~0.0.2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -563,7 +563,7 @@ it("should add dependency (GitHub)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "mishoo/UglifyJS#v3.14.1"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -650,7 +650,7 @@ it("should add dependency alongside workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "baz"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -724,7 +724,7 @@ it("should add aliased dependency (npm)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "bar@npm:baz@~0.0.2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -785,7 +785,7 @@ it("should add aliased dependency (GitHub)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "uglify@mishoo/UglifyJS#v3.14.1"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -870,7 +870,7 @@ it("should let you add the same package twice", async () => { } = spawn({ cmd: [bunExe(), "add", "baz@0.0.3"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -922,7 +922,7 @@ it("should let you add the same package twice", async () => { } = spawn({ cmd: [bunExe(), "add", "baz", "-d"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -986,7 +986,7 @@ it("should install version tagged with `latest` by default", async () => { } = spawn({ cmd: [bunExe(), "add", "baz"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1039,7 +1039,7 @@ it("should install version tagged with `latest` by default", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1100,7 +1100,7 @@ it("should handle Git URL in dependencies (SCP-style)", async () => { } = spawn({ cmd: [bunExe(), "add", "bun@github.com:mishoo/UglifyJS.git"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1166,7 +1166,7 @@ it("should handle Git URL in dependencies (SCP-style)", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1199,7 +1199,7 @@ it("should not save git urls twice", async () => { const { exited: exited1 } = spawn({ cmd: [bunExe(), "add", "https://github.com/liz3/empty-bun-repo"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1215,7 +1215,7 @@ it("should not save git urls twice", async () => { const { exited: exited2 } = spawn({ cmd: [bunExe(), "add", "https://github.com/liz3/empty-bun-repo"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1248,7 +1248,7 @@ it("should prefer optionalDependencies over dependencies of the same name", asyn const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "bar@0.0.2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1308,7 +1308,7 @@ it("should prefer dependencies over peerDependencies of the same name", async () const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "bar@0.0.2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1366,7 +1366,7 @@ it("should add dependency without duplication", async () => { } = spawn({ cmd: [bunExe(), "add", "bar"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1415,7 +1415,7 @@ it("should add dependency without duplication", async () => { } = spawn({ cmd: [bunExe(), "add", "bar"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1472,7 +1472,7 @@ it("should add dependency without duplication (GitHub)", async () => { } = spawn({ cmd: [bunExe(), "add", "mishoo/UglifyJS#v3.14.1"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1533,7 +1533,7 @@ it("should add dependency without duplication (GitHub)", async () => { } = spawn({ cmd: [bunExe(), "add", "mishoo/UglifyJS#v3.14.1"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1616,7 +1616,7 @@ it("should add dependencies to workspaces directly", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: join(package_dir, "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1679,7 +1679,7 @@ async function installRedirectsToAdd(saveFlagFirst: boolean) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", ...args], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1716,7 +1716,7 @@ it("should add dependency alongside peerDependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", "bar"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1770,7 +1770,7 @@ it("should add local tarball dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", tarball], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/bun-create.test.ts b/test/cli/install/bun-create.test.ts index 97ecb897149c74..445af2cf2c37c1 100644 --- a/test/cli/install/bun-create.test.ts +++ b/test/cli/install/bun-create.test.ts @@ -40,7 +40,7 @@ it("should create selected template with @ prefix", async () => { const { stderr } = spawn({ cmd: [bunExe(), "create", "@quick-start/some-template"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -56,7 +56,7 @@ it("should create selected template with @ prefix implicit `/create`", async () const { stderr } = spawn({ cmd: [bunExe(), "create", "@second-quick-start"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -72,7 +72,7 @@ it("should create selected template with @ prefix implicit `/create` with versio const { stderr } = spawn({ cmd: [bunExe(), "create", "@second-quick-start"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -92,7 +92,7 @@ it("should create template from local folder", async () => { const { exited } = spawn({ cmd: [bunExe(), "create", testTemplate], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { ...env, BUN_CREATE_DIR: bunCreateDir }, diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index fb6db756d14e28..781de0cfc82ecd 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -66,7 +66,7 @@ describe("chooses", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -158,7 +158,7 @@ registry = "http://${server.hostname}:${server.port}/" const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -192,7 +192,7 @@ it("should handle missing package", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "foo"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -244,7 +244,7 @@ foo = { token = "bar" } const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "@foo/bar"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -282,7 +282,7 @@ it("should handle empty string in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -362,7 +362,7 @@ it("should handle workspaces", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -411,7 +411,7 @@ it("should handle workspaces", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -477,7 +477,7 @@ it("should handle `workspace:` specifier", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -507,7 +507,7 @@ it("should handle `workspace:` specifier", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -550,7 +550,7 @@ it("should handle workspaces with packages array", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -609,7 +609,7 @@ it("should handle inter-dependency between workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -668,7 +668,7 @@ it("should handle inter-dependency between workspaces (devDependencies)", async const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -727,7 +727,7 @@ it("should handle inter-dependency between workspaces (optionalDependencies)", a const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -785,7 +785,7 @@ it("should ignore peerDependencies within workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -828,7 +828,7 @@ it("should handle installing the same peerDependency with different versions", a const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -867,7 +867,7 @@ it("should handle installing the same peerDependency with the same version", asy const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -917,7 +917,7 @@ it("should handle life-cycle scripts within workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -982,7 +982,7 @@ it("should handle life-cycle scripts during re-installation", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1015,7 +1015,7 @@ it("should handle life-cycle scripts during re-installation", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1049,7 +1049,7 @@ it("should handle life-cycle scripts during re-installation", async () => { } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1109,7 +1109,7 @@ it("should use updated life-cycle scripts in root during re-installation", async } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1159,7 +1159,7 @@ it("should use updated life-cycle scripts in root during re-installation", async } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1195,7 +1195,7 @@ it("should use updated life-cycle scripts in root during re-installation", async } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1256,7 +1256,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1309,7 +1309,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1348,7 +1348,7 @@ it("should use updated life-cycle scripts in dependency during re-installation", } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1396,7 +1396,7 @@ it("should ignore workspaces within workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1435,7 +1435,7 @@ it("should handle ^0 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1479,7 +1479,7 @@ it("should handle ^1 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1516,7 +1516,7 @@ it("should handle ^0.0 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1560,7 +1560,7 @@ it("should handle ^0.1 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1597,7 +1597,7 @@ it("should handle ^0.0.0 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1634,7 +1634,7 @@ it("should handle ^0.0.2 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1702,7 +1702,7 @@ it("should handle matching workspaces from dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1736,7 +1736,7 @@ it("should edit package json correctly with git dependencies", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "dylan-conway/install-test2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1756,7 +1756,7 @@ it("should edit package json correctly with git dependencies", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "dylan-conway/install-test2#HEAD"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1776,7 +1776,7 @@ it("should edit package json correctly with git dependencies", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "github:dylan-conway/install-test2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1796,7 +1796,7 @@ it("should edit package json correctly with git dependencies", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "github:dylan-conway/install-test2#HEAD"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1831,7 +1831,7 @@ it("should handle ^0.0.2-rc in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1876,7 +1876,7 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1921,7 +1921,7 @@ it("should choose the right version with prereleases", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1966,7 +1966,7 @@ it("should handle ^0.0.2rc1 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2011,7 +2011,7 @@ it("should handle ^0.0.2_pre3 in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2056,7 +2056,7 @@ it("should handle ^0.0.2b_4+cafe_b0ba in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2101,7 +2101,7 @@ it("should handle caret range in dependencies when the registry has prereleased const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2160,7 +2160,7 @@ it("should prefer latest-tagged dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2213,7 +2213,7 @@ it("should install latest with prereleases", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "baz"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2246,7 +2246,7 @@ it("should install latest with prereleases", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2278,7 +2278,7 @@ it("should install latest with prereleases", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2311,7 +2311,7 @@ it("should install latest with prereleases", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2355,7 +2355,7 @@ it("should handle dependency aliasing", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2412,7 +2412,7 @@ it("should handle dependency aliasing (versioned)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2469,7 +2469,7 @@ it("should handle dependency aliasing (dist-tagged)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2530,7 +2530,7 @@ it("should not reinstall aliased dependencies", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2570,7 +2570,7 @@ it("should not reinstall aliased dependencies", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2637,7 +2637,7 @@ it("should handle aliased & direct dependency references", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2719,7 +2719,7 @@ it("should not hoist if name collides with alias", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2799,7 +2799,7 @@ it("should get npm alias with matching version", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2853,7 +2853,7 @@ it("should not apply overrides to package name of aliased package", async () => const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2900,7 +2900,7 @@ it("should handle unscoped alias on scoped dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -2961,7 +2961,7 @@ it("should handle scoped alias on unscoped dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3032,7 +3032,7 @@ it("should handle aliased dependency with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3094,7 +3094,7 @@ it("should handle aliased dependency with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3161,7 +3161,7 @@ it("should handle GitHub URL in dependencies (user/repo)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3214,7 +3214,7 @@ it("should handle GitHub URL in dependencies (user/repo#commit-id)", async () => const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3281,7 +3281,7 @@ it("should handle GitHub URL in dependencies (user/repo#tag)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3355,7 +3355,7 @@ it("should handle bitbucket git dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3392,7 +3392,7 @@ it("should handle bitbucket git dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3434,7 +3434,7 @@ it("should handle gitlab git dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3471,7 +3471,7 @@ it("should handle gitlab git dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "add", dep], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3511,7 +3511,7 @@ it("should handle GitHub URL in dependencies (github:user/repo#tag)", async () = const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3579,7 +3579,7 @@ it("should handle GitHub URL in dependencies (https://github.com/user/repo.git)" const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3632,7 +3632,7 @@ it("should handle GitHub URL in dependencies (git://github.com/user/repo.git#com const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3700,7 +3700,7 @@ it("should handle GitHub URL in dependencies (git+https://github.com/user/repo.g const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3753,7 +3753,7 @@ it("should handle GitHub tarball URL in dependencies (https://github.com/user/re const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3810,7 +3810,7 @@ it("should handle GitHub tarball URL in dependencies (https://github.com/user/re const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { @@ -3875,7 +3875,7 @@ it("should treat non-GitHub http(s) URLs as tarballs (https://some.url/path?stuf const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3933,7 +3933,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3991,7 +3991,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4093,7 +4093,7 @@ it("should consider peerDependencies during hoisting", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4193,7 +4193,7 @@ it("should install peerDependencies when needed", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4258,7 +4258,7 @@ it("should not regard peerDependencies declarations as duplicates", async () => const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4291,7 +4291,7 @@ it("should report error on invalid format for package.json", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4317,7 +4317,7 @@ it("should report error on invalid format for dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4343,7 +4343,7 @@ it("should report error on invalid format for optionalDependencies", async () => const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4371,7 +4371,7 @@ it("should report error on invalid format for workspaces", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4413,7 +4413,7 @@ it("should report error on duplicated workspace packages", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4448,7 +4448,7 @@ it("should handle Git URL in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4510,7 +4510,7 @@ it("should handle Git URL in dependencies (SCP-style)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4570,7 +4570,7 @@ it("should handle Git URL with committish in dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4632,7 +4632,7 @@ it("should fail on invalid Git URL", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4670,7 +4670,7 @@ it("should fail on Git URL with invalid committish", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4711,7 +4711,7 @@ it("should de-duplicate committish in Git URLs", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4809,7 +4809,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4867,7 +4867,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4942,7 +4942,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5016,7 +5016,7 @@ it("should prefer optionalDependencies over dependencies of the same name", asyn const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5070,7 +5070,7 @@ it("should prefer dependencies over peerDependencies of the same name", async () const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5116,7 +5116,7 @@ it("should handle tarball URL", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5165,7 +5165,7 @@ it("should handle tarball path", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5213,7 +5213,7 @@ it("should handle tarball URL with aliasing", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5262,7 +5262,7 @@ it("should handle tarball path with aliasing", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5320,7 +5320,7 @@ it("should de-duplicate dependencies alongside tarball URL", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5404,7 +5404,7 @@ it("should handle tarball URL with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5466,7 +5466,7 @@ it("should handle tarball URL with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5549,7 +5549,7 @@ it("should handle tarball path with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5610,7 +5610,7 @@ it("should handle tarball path with existing lockfile", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5688,7 +5688,7 @@ it("should handle devDependencies from folder", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5744,7 +5744,7 @@ it("should deduplicate devDependencies from folder", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5798,7 +5798,7 @@ it("should install dependencies in root package of workspace", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5851,7 +5851,7 @@ it("should install dependencies in root package of workspace (*)", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5903,7 +5903,7 @@ it("should ignore invalid workspaces from parent directory", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5960,7 +5960,7 @@ it("should handle --cwd", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cwd", "moo"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6005,7 +6005,7 @@ it("should handle --frozen-lockfile", async () => { const { stderr, exited } = spawn({ cmd: [bunExe(), "install", "--frozen-lockfile"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6033,7 +6033,7 @@ frozenLockfile = true const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6070,7 +6070,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6212,7 +6212,7 @@ cache = false } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6270,7 +6270,7 @@ it("should handle trustedDependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6325,7 +6325,7 @@ it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6356,7 +6356,7 @@ it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6400,7 +6400,7 @@ it("should handle `workspaces:bar` and `workspace:*` gracefully", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6444,7 +6444,7 @@ it("should handle `workspaces:*` and `workspace:bar` gracefully", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6488,7 +6488,7 @@ it("should handle `workspaces:bar` and `workspace:bar` gracefully", async () => const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6543,7 +6543,7 @@ it("should handle installing packages from inside a workspace with `*`", async ( } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "yolo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6574,7 +6574,7 @@ it("should handle installing packages from inside a workspace with `*`", async ( } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "yolo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6626,7 +6626,7 @@ it("should handle installing packages from inside a workspace without prefix", a } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "p1"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6654,7 +6654,7 @@ it("should handle installing packages from inside a workspace without prefix", a } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "p1"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6799,7 +6799,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "package2"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6831,7 +6831,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "package2"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6857,7 +6857,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "package3"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6887,7 +6887,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "package3"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6912,7 +6912,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "package4"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6942,7 +6942,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "package4"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6967,7 +6967,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir, "packages", "package5"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -6997,7 +6997,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir, "packages", "package5"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7023,7 +7023,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install"], cwd: join(package_dir), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7053,7 +7053,7 @@ it("should handle installing packages inside workspaces with difference versions } = spawn({ cmd: [bunExe(), "install", "bar"], cwd: join(package_dir), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7096,7 +7096,7 @@ it("should override npm dependency by matching workspace", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7143,7 +7143,7 @@ it("should not override npm dependency by workspace with mismatched version", as const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7187,7 +7187,7 @@ it("should override @scoped npm dependency by matching workspace", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7237,7 +7237,7 @@ it("should override aliased npm dependency by matching workspace", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7292,7 +7292,7 @@ it("should override child npm dependency by matching workspace", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7350,7 +7350,7 @@ it("should not override child npm dependency by workspace with mismatched versio const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7414,7 +7414,7 @@ it("should override @scoped child npm dependency by matching workspace", async ( const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7477,7 +7477,7 @@ it("should override aliased child npm dependency by matching workspace", async ( const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7542,7 +7542,7 @@ it("should handle `workspace:` with semver range", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7599,7 +7599,7 @@ it("should handle `workspace:` with alias & @scope", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7673,7 +7673,7 @@ it("should handle `workspace:*` on both root & child", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7711,7 +7711,7 @@ it("should handle `workspace:*` on both root & child", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7758,7 +7758,7 @@ it("should install peer dependencies from root package", async () => { cmd: [bunExe(), "install"], cwd: package_dir, env, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", }); @@ -7799,7 +7799,7 @@ it("should install correct version of peer dependency from root package", async cmd: [bunExe(), "install"], cwd: package_dir, env, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", }); @@ -7871,7 +7871,7 @@ describe("Registry URLs", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7914,7 +7914,7 @@ describe("Registry URLs", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -7955,7 +7955,7 @@ describe("Registry URLs", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index b9625c1eba4b1f..b3417fdeaa3355 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -61,7 +61,7 @@ it("should link and unlink workspace package", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -83,7 +83,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "link"], cwd: join(link_dir, "packages", "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -99,7 +99,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "link", "moo"], cwd: join(link_dir, "packages", "boba"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -124,7 +124,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "unlink"], cwd: join(link_dir, "packages", "moo"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -141,7 +141,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "link"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -157,7 +157,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "link", "foo"], cwd: join(link_dir, "packages", "boba"), - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -183,7 +183,7 @@ it("should link and unlink workspace package", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "unlink"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -221,7 +221,7 @@ it("should link package", async () => { } = spawn({ cmd: [bunExe(), "link"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -240,7 +240,7 @@ it("should link package", async () => { } = spawn({ cmd: [bunExe(), "link", link_name], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -265,7 +265,7 @@ it("should link package", async () => { } = spawn({ cmd: [bunExe(), "unlink"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -284,7 +284,7 @@ it("should link package", async () => { } = spawn({ cmd: [bunExe(), "link", link_name], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -321,7 +321,7 @@ it("should link scoped package", async () => { } = spawn({ cmd: [bunExe(), "link"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -340,7 +340,7 @@ it("should link scoped package", async () => { } = spawn({ cmd: [bunExe(), "link", link_name], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -365,7 +365,7 @@ it("should link scoped package", async () => { } = spawn({ cmd: [bunExe(), "unlink"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -384,7 +384,7 @@ it("should link scoped package", async () => { } = spawn({ cmd: [bunExe(), "link", link_name], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -428,7 +428,7 @@ it("should link dependency without crashing", async () => { } = spawn({ cmd: [bunExe(), "link"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -447,7 +447,7 @@ it("should link dependency without crashing", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -479,7 +479,7 @@ it("should link dependency without crashing", async () => { } = spawn({ cmd: [bunExe(), "unlink"], cwd: link_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -498,7 +498,7 @@ it("should link dependency without crashing", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/bun-pm.test.ts b/test/cli/install/bun-pm.test.ts index 348fa46a55ca83..050dff32dd07b8 100644 --- a/test/cli/install/bun-pm.test.ts +++ b/test/cli/install/bun-pm.test.ts @@ -51,7 +51,7 @@ it("should list top-level dependency", async () => { await spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -63,7 +63,7 @@ it("should list top-level dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "ls"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -107,7 +107,7 @@ it("should list all dependencies", async () => { await spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -119,7 +119,7 @@ it("should list all dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "ls", "--all"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -164,7 +164,7 @@ it("should list top-level aliased dependency", async () => { await spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -176,7 +176,7 @@ it("should list top-level aliased dependency", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "ls"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -220,7 +220,7 @@ it("should list aliased dependencies", async () => { await spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -232,7 +232,7 @@ it("should list aliased dependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "ls", "--all"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -278,7 +278,7 @@ it("should remove all cache", async () => { await spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { diff --git a/test/cli/install/bun-remove.test.ts b/test/cli/install/bun-remove.test.ts index 2a302821afe9c7..da8d7d5a3959d0 100644 --- a/test/cli/install/bun-remove.test.ts +++ b/test/cli/install/bun-remove.test.ts @@ -54,7 +54,7 @@ it("should remove existing package", async () => { const { exited: exited1 } = spawn({ cmd: [bunExe(), "add", `file:${pkg1_path}`.replace(/\\/g, "\\\\")], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -63,7 +63,7 @@ it("should remove existing package", async () => { const { exited: exited2 } = spawn({ cmd: [bunExe(), "add", `file:${pkg2_path}`.replace(/\\/g, "\\\\")], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -91,7 +91,7 @@ it("should remove existing package", async () => { } = spawn({ cmd: [bunExe(), "remove", "pkg1"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -132,7 +132,7 @@ it("should remove existing package", async () => { } = spawn({ cmd: [bunExe(), "remove", "pkg2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -181,7 +181,7 @@ it("should not reject missing package", async () => { const { exited: addExited } = spawn({ cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -191,7 +191,7 @@ it("should not reject missing package", async () => { const { exited: rmExited } = spawn({ cmd: [bunExe(), "remove", "pkg2"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -211,7 +211,7 @@ it("should not affect if package is not installed", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "remove", "pkg"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -248,7 +248,7 @@ it("should retain a new line in the end of package.json", async () => { const { exited: addExited } = spawn({ cmd: [bunExe(), "add", `file:${pkg_path}`.replace(/\\/g, "\\\\")], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -261,7 +261,7 @@ it("should retain a new line in the end of package.json", async () => { const { exited } = spawn({ cmd: [bunExe(), "remove", "pkg"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -294,7 +294,7 @@ it("should remove peerDependencies", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "remove", "bar"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 64f765b97f5c00..39b5f1931da5b6 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -126,16 +126,10 @@ for (let withRun of [false, true]) { it("exit signal works", async () => { { - let signalCode: any; - let exitCode: any; - const { stdout, stderr } = spawnSync({ + const { stdout, stderr, exitCode, signalCode } = spawnSync({ cmd: [bunExe(), "run", "bash", "-c", "kill -4 $$"], cwd: run_dir, env: bunEnv, - onExit(subprocess, exitCode2, signalCode2, error) { - exitCode = exitCode2; - signalCode = signalCode2; - }, }); expect(stderr.toString()).toBe(""); @@ -143,16 +137,10 @@ for (let withRun of [false, true]) { expect(exitCode).toBe(null); } { - let signalCode: any; - let exitCode: any; - const { stdout, stderr } = spawnSync({ + const { stdout, stderr, exitCode, signalCode } = spawnSync({ cmd: [bunExe(), "run", "bash", "-c", "kill -9 $$"], cwd: run_dir, env: bunEnv, - onExit(subprocess, exitCode2, signalCode2, error) { - exitCode = exitCode2; - signalCode = signalCode2; - }, }); expect(stderr.toString()).toBe(""); @@ -269,7 +257,7 @@ console.log(minify("print(6 * 7)").code); } = spawn({ cmd: [bunExe(), "run", "test.js"], cwd: run_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { @@ -295,7 +283,7 @@ console.log(minify("print(6 * 7)").code); } = spawn({ cmd: [bunExe(), "test.js"], cwd: run_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { @@ -335,7 +323,7 @@ for (const entry of await decompress(Buffer.from(buffer))) { } = spawn({ cmd: [bunExe(), "test.js"], cwd: run_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { @@ -375,7 +363,7 @@ for (const entry of await decompress(Buffer.from(buffer))) { } = spawn({ cmd: [bunExe(), "run", "test.js"], cwd: run_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: { diff --git a/test/cli/install/bun-update.test.ts b/test/cli/install/bun-update.test.ts index 29dcec610d05d8..aa46064c22009c 100644 --- a/test/cli/install/bun-update.test.ts +++ b/test/cli/install/bun-update.test.ts @@ -58,7 +58,7 @@ it("should update to latest version of dependency", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -102,7 +102,7 @@ it("should update to latest version of dependency", async () => { } = spawn({ cmd: [bunExe(), "update", "baz"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -171,7 +171,7 @@ it("should update to latest versions of dependencies", async () => { } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -223,7 +223,7 @@ it("should update to latest versions of dependencies", async () => { } = spawn({ cmd: [bunExe(), "update"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -289,7 +289,7 @@ it("lockfile should not be modified when there are no version changes, issue#588 const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -312,7 +312,7 @@ it("lockfile should not be modified when there are no version changes, issue#588 const { exited } = spawn({ cmd: [bunExe(), "update"], cwd: package_dir, // package.json is not changed - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/bun-upgrade.test.ts b/test/cli/install/bun-upgrade.test.ts index 12e95ae11a2e5a..c40d378bcade37 100644 --- a/test/cli/install/bun-upgrade.test.ts +++ b/test/cli/install/bun-upgrade.test.ts @@ -7,7 +7,7 @@ import { join } from "path"; import { copyFileSync } from "js/node/fs/export-star-from"; let run_dir: string; -let exe_name: string = "bun-debug"; +let exe_name: string = "bun-debug" + (process.platform === "win32" ? ".exe" : ""); beforeEach(async () => { run_dir = await realpath( @@ -20,6 +20,7 @@ afterEach(async () => { }); it("two invalid arguments, should display error message and suggest command", async () => { + console.log(run_dir, exe_name); const { stderr } = spawn({ cmd: [join(run_dir, exe_name), "upgrade", "bun-types", "--dev"], cwd: run_dir, diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index e7bbb8a99afcf2..d7364397a51c0c 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -40,7 +40,7 @@ it("should install and run default (latest) version", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "x", "uglify-js", "--compress"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: new TextEncoder().encode("console.log(6 * 7);"), stderr: "pipe", env, @@ -58,7 +58,7 @@ it("should install and run specified version", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "x", "uglify-js@3.14.1", "-v"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -76,7 +76,7 @@ it("should output usage if no arguments are passed", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "x"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -97,7 +97,7 @@ it("should work for @scoped packages", async () => { const withoutCache = spawn({ cmd: [bunExe(), "x", "@withfig/autocomplete-tools", "--help"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -115,7 +115,7 @@ it("should work for @scoped packages", async () => { const cached = spawn({ cmd: [bunExe(), "x", "@withfig/autocomplete-tools", "--help"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -143,7 +143,7 @@ console.log( const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "--bun", "x", "uglify-js", "test.js", "--compress"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -164,7 +164,7 @@ it("should work for github repository", async () => { const withoutCache = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay", "--help"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -182,7 +182,7 @@ it("should work for github repository", async () => { const cached = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay", "--help"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -202,7 +202,7 @@ it("should work for github repository with committish", async () => { const withoutCache = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -220,7 +220,7 @@ it("should work for github repository with committish", async () => { const cached = spawn({ cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"], cwd: x_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 0125fee597c68f..2a741a08a593f4 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -64,7 +64,7 @@ test("basic 1", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -93,7 +93,7 @@ test("basic 1", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -129,7 +129,7 @@ test("dependency from root satisfies range from dependency", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -160,7 +160,7 @@ test("dependency from root satisfies range from dependency", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -197,7 +197,7 @@ test("peerDependency in child npm dependency should not maintain old version whe var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -238,7 +238,7 @@ test("peerDependency in child npm dependency should not maintain old version whe ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -277,7 +277,7 @@ test("package added after install", async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -319,7 +319,7 @@ test("package added after install", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -355,7 +355,7 @@ test("package added after install", async () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -462,7 +462,7 @@ test("it should re-symlink binaries that become invalid when updating package ve var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -502,7 +502,7 @@ test("it should re-symlink binaries that become invalid when updating package ve ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -1759,7 +1759,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -1808,7 +1808,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -1862,7 +1862,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -1950,7 +1950,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2020,7 +2020,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2052,7 +2052,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2101,7 +2101,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2143,7 +2143,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2169,7 +2169,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2201,7 +2201,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2225,7 +2225,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2260,7 +2260,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2295,7 +2295,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2320,7 +2320,7 @@ for (const forceWaiterThread of [false, true]) { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2375,7 +2375,7 @@ for (const forceWaiterThread of [false, true]) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2787,7 +2787,7 @@ for (const forceWaiterThread of [false, true]) { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: testEnv, @@ -2971,7 +2971,7 @@ for (const forceWaiterThread of [false, true]) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3955,7 +3955,7 @@ describe("semver", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -3993,7 +3993,7 @@ describe("semver", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4121,7 +4121,7 @@ for (let i = 0; i < prereleaseTests.length; i++) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4263,7 +4263,7 @@ for (let i = 0; i < prereleaseFailTests.length; i++) { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4298,7 +4298,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4389,7 +4389,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4442,7 +4442,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4510,7 +4510,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4590,7 +4590,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4725,7 +4725,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4771,7 +4771,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4803,7 +4803,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4855,7 +4855,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4896,7 +4896,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -4974,7 +4974,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5037,7 +5037,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5090,7 +5090,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5131,7 +5131,7 @@ describe("yarn tests", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5172,7 +5172,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5248,7 +5248,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5277,7 +5277,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5309,7 +5309,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5338,7 +5338,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5371,7 +5371,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5401,7 +5401,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5428,7 +5428,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5458,7 +5458,7 @@ describe("yarn tests", () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, @@ -5484,7 +5484,7 @@ describe("yarn tests", () => { ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env, diff --git a/test/cli/run/run-quote.test.ts b/test/cli/run/run-quote.test.ts index 80437d2f1b516f..a6dffbd6a71b45 100644 --- a/test/cli/run/run-quote.test.ts +++ b/test/cli/run/run-quote.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { expect, it } from "bun:test"; import { bunRunAsScript, tempDirWithFiles } from "harness"; diff --git a/test/harness.ts b/test/harness.ts index 4b5f7b10cc874a..97dfe5cec855d0 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -21,6 +21,20 @@ export const bunEnv: NodeJS.ProcessEnv = { BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0", }; +if (isWindows) { + bunEnv.SHELLOPTS = "igncr"; // Ignore carriage return +} + +for (let key in bunEnv) { + if (bunEnv[key] === undefined) { + delete bunEnv[key]; + } + + if (key.startsWith("BUN_DEBUG_") && key !== "BUN_DEBUG_QUIET_LOGS") { + delete bunEnv[key]; + } +} + export function bunExe() { return process.execPath; } @@ -439,3 +453,74 @@ export async function describeWithContainer( export function osSlashes(path: string) { return isWindows ? path.replace(/\//g, "\\") : path; } + +import * as child_process from "node:child_process"; + +class WriteBlockedError extends Error { + constructor(time) { + super("Write blocked for " + (time | 0) + "ms"); + this.name = "WriteBlockedError"; + } +} +function failTestsOnBlockingWriteCall() { + const prop = Object.getOwnPropertyDescriptor(child_process.ChildProcess.prototype, "stdin"); + const didAttachSymbol = Symbol("kDidAttach"); + if (prop) { + Object.defineProperty(child_process.ChildProcess.prototype, "stdin", { + ...prop, + get() { + const actual = prop.get.call(this); + if (actual?.write && !actual.__proto__[didAttachSymbol]) { + actual.__proto__[didAttachSymbol] = true; + attachWriteMeasurement(actual); + } + return actual; + }, + }); + } + + function attachWriteMeasurement(stream) { + const prop = Object.getOwnPropertyDescriptor(stream.__proto__, "write"); + if (prop) { + Object.defineProperty(stream.__proto__, "write", { + ...prop, + value(chunk, encoding, cb) { + const start = performance.now(); + const rc = prop.value.apply(this, arguments); + const end = performance.now(); + if (end - start > 8) { + const err = new WriteBlockedError(end - start); + throw err; + } + return rc; + }, + }); + } + } +} + +failTestsOnBlockingWriteCall(); + +import { heapStats } from "bun:jsc"; +export function dumpStats() { + const stats = heapStats(); + const { objectTypeCounts, protectedObjectTypeCounts } = stats; + console.log({ + objects: Object.fromEntries(Object.entries(objectTypeCounts).sort()), + protected: Object.fromEntries(Object.entries(protectedObjectTypeCounts).sort()), + }); +} + +export function fillRepeating(dstBuffer: NodeJS.TypedArray, start: number, end: number) { + let len = dstBuffer.length, // important: use indices length, not byte-length + sLen = end - start, + p = sLen; // set initial position = source sequence length + + // step 2: copy existing data doubling segment length per iteration + while (p < len) { + if (p + sLen > len) sLen = len - p; // if not power of 2, truncate last segment + dstBuffer.copyWithin(p, start, sLen); // internal copy + p += sLen; // add current length to offset + sLen <<= 1; // double length for next segment + } +} diff --git a/test/integration/sharp/sharp.test.ts b/test/integration/sharp/sharp.test.ts index 291d4f4c7e0063..9c70cffc81a06e 100644 --- a/test/integration/sharp/sharp.test.ts +++ b/test/integration/sharp/sharp.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, expect, it } from "bun:test"; import path from "path"; import sharp from "sharp"; diff --git a/test/js/bun/dns/resolve-dns.test.ts b/test/js/bun/dns/resolve-dns.test.ts index 80739386572038..48352278278c45 100644 --- a/test/js/bun/dns/resolve-dns.test.ts +++ b/test/js/bun/dns/resolve-dns.test.ts @@ -1,4 +1,4 @@ -import { dns } from "bun"; +import { SystemError, dns } from "bun"; import { describe, expect, it, test } from "bun:test"; import { withoutAggressiveGC } from "harness"; import { isIP, isIPv4, isIPv6 } from "node:net"; @@ -7,7 +7,7 @@ const backends = ["system", "libc", "c-ares"]; const validHostnames = ["localhost", "example.com"]; const invalidHostnames = ["adsfa.asdfasdf.asdf.com"]; // known invalid const malformedHostnames = ["", " ", ".", " .", "localhost:80", "this is not a hostname"]; - +const isWindows = process.platform === "win32"; describe("dns", () => { describe.each(backends)("lookup() [backend: %s]", backend => { describe.each(validHostnames)("%s", hostname => { @@ -45,6 +45,23 @@ describe("dns", () => { address: isIP, }, ])("%j", async ({ options, address: expectedAddress, family: expectedFamily }) => { + // this behavior matchs nodejs + const expect_to_fail = + isWindows && + backend !== "c-ares" && + (options.family === "IPv6" || options.family === 6) && + hostname !== "localhost"; + if (expect_to_fail) { + try { + // @ts-expect-error + await dns.lookup(hostname, options); + expect.unreachable(); + } catch (err: unknown) { + expect(err).toBeDefined(); + expect((err as SystemError).code).toBe("DNS_ENOTFOUND"); + } + return; + } // @ts-expect-error const result = await dns.lookup(hostname, options); expect(result).toBeArray(); diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index dc1aa60ecd1b32..846dd00e0296cf 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -377,9 +377,10 @@ describe("Server", () => { cmd: [bunExe(), path.join("js-sink-sourmap-fixture", "index.mjs")], cwd: import.meta.dir, env: bunEnv, - stderr: "pipe", + stdin: "inherit", + stderr: "inherit", + stdout: "inherit", }); - expect(stderr).toBeEmpty(); expect(exitCode).toBe(0); }); diff --git a/test/js/bun/http/fetch-file-upload.test.ts b/test/js/bun/http/fetch-file-upload.test.ts index f79bd5526c4c36..4efedfe778e719 100644 --- a/test/js/bun/http/fetch-file-upload.test.ts +++ b/test/js/bun/http/fetch-file-upload.test.ts @@ -1,4 +1,4 @@ -import { expect, test, describe } from "bun:test"; +import { expect, test } from "bun:test"; import { withoutAggressiveGC } from "harness"; import { tmpdir } from "os"; import { join } from "path"; @@ -77,7 +77,7 @@ test("req.formData throws error when stream is in use", async () => { development: false, error(fail) { pass = true; - if (fail.toString().includes("is already used")) { + if (fail.toString().includes("already used")) { return new Response("pass"); } return new Response("fail"); @@ -100,6 +100,7 @@ test("req.formData throws error when stream is in use", async () => { // but it does for Response expect(await res.text()).toBe("pass"); + expect(pass).toBe(true); server.stop(true); }); @@ -133,33 +134,32 @@ test("formData uploads roundtrip, without a call to .body", async () => { }); test("uploads roundtrip with sendfile()", async () => { - var hugeTxt = "huge".repeat(1024 * 1024 * 32); + const hugeTxt = Buffer.allocUnsafe(1024 * 1024 * 32 * "huge".length); + hugeTxt.fill("huge"); + const hash = Bun.CryptoHasher.hash("sha256", hugeTxt, "hex"); + const path = join(tmpdir(), "huge.txt"); require("fs").writeFileSync(path, hugeTxt); - const server = Bun.serve({ port: 0, development: false, - maxRequestBodySize: 1024 * 1024 * 1024 * 8, + maxRequestBodySize: hugeTxt.byteLength * 2, async fetch(req) { - var count = 0; + const hasher = new Bun.CryptoHasher("sha256"); for await (let chunk of req.body!) { - count += chunk.byteLength; + hasher.update(chunk); } - return new Response(count + ""); + return new Response(hasher.digest("hex")); }, }); - const resp = await fetch("http://" + server.hostname + ":" + server.port, { + const resp = await fetch(server.url, { body: Bun.file(path), method: "PUT", }); expect(resp.status).toBe(200); - - const body = parseInt(await resp.text()); - expect(body).toBe(hugeTxt.length); - + expect(await resp.text()).toBe(hash); server.stop(true); }); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 0a6c07e44a4162..01ff72e2e11189 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -3,11 +3,12 @@ import { file, gc, Serve, serve, Server } from "bun"; import { afterEach, describe, it, expect, afterAll } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; import { join, resolve } from "path"; -import { bunExe, bunEnv } from "harness"; +import { bunExe, bunEnv, dumpStats } from "harness"; // import { renderToReadableStream } from "react-dom/server"; // import app_jsx from "./app.jsx"; import { spawn } from "child_process"; import { tmpdir } from "os"; +import { heapStats } from "bun:jsc"; let renderToReadableStream: any = null; let app_jsx: any = null; @@ -27,7 +28,7 @@ async function runTest({ port, ...serverOptions }: Serve, test: (server: Se while (!server) { try { server = serve({ ...serverOptions, port: 0 }); - console.log("server=", server); + console.log(`Server: ${server.url}`); break; } catch (e: any) { console.log("catch:", e); @@ -38,18 +39,95 @@ async function runTest({ port, ...serverOptions }: Serve, test: (server: Se } } - console.log("before test(server)"); await test(server); } afterAll(() => { - console.log("afterAll"); if (server) { server.stop(true); server = undefined; } }); +describe("1000 simultaneous uploads & downloads do not leak ReadableStream", () => { + for (let isDirect of [true, false] as const) { + it( + isDirect ? "direct" : "default", + async () => { + const blob = new Blob([new Uint8Array(1024 * 768).fill(123)]); + Bun.gc(true); + + const expected = Bun.CryptoHasher.hash("sha256", blob, "base64"); + const initialCount = heapStats().objectTypeCounts.ReadableStream || 0; + + await runTest( + { + async fetch(req) { + var hasher = new Bun.SHA256(); + for await (const chunk of req.body) { + await Bun.sleep(0); + hasher.update(chunk); + } + return new Response( + isDirect + ? new ReadableStream({ + type: "direct", + async pull(controller) { + await Bun.sleep(0); + controller.write(Buffer.from(hasher.digest("base64"))); + await controller.flush(); + controller.close(); + }, + }) + : new ReadableStream({ + async pull(controller) { + await Bun.sleep(0); + controller.enqueue(Buffer.from(hasher.digest("base64"))); + controller.close(); + }, + }), + ); + }, + }, + async server => { + const count = 1000; + async function callback() { + const response = await fetch(server.url, { body: blob, method: "POST" }); + + // We are testing for ReadableStream leaks, so we use the ReadableStream here. + const chunks = []; + for await (const chunk of response.body) { + chunks.push(chunk); + } + + const digest = Buffer.from(Bun.concatArrayBuffers(chunks)).toString(); + + expect(digest).toBe(expected); + Bun.gc(false); + } + { + const promises = new Array(count); + for (let i = 0; i < count; i++) { + promises[i] = callback(); + } + + await Promise.all(promises); + } + + Bun.gc(true); + dumpStats(); + expect(heapStats().objectTypeCounts.ReadableStream).toBeWithin( + Math.max(initialCount - count / 2, 0), + initialCount + count / 2, + ); + }, + ); + }, + 100000, + ); + } +}); + [200, 200n, 303, 418, 599, 599n].forEach(statusCode => { it(`should response with HTTP status code (${statusCode})`, async () => { await runTest( @@ -59,7 +137,7 @@ afterAll(() => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(Number(statusCode)); expect(await response.text()).toBe("Foo Bar"); }, @@ -81,7 +159,7 @@ afterAll(() => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(500); expect(await response.text()).toBe("Error!"); }, @@ -98,7 +176,7 @@ it("should display a welcome message when the response value type is incorrect", }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); const text = await response.text(); expect(text).toContain("Welcome to Bun!"); }, @@ -122,7 +200,7 @@ it("request.signal works in trivial case", async () => { }, async server => { try { - await fetch(`http://${server.hostname}:${server.port}`, { signal: aborty.signal }); + await fetch(server.url.origin, { signal: aborty.signal }); throw new Error("Expected fetch to throw"); } catch (e: any) { expect(e.name).toBe("AbortError"); @@ -147,16 +225,14 @@ it("request.signal works in leaky case", async () => { expect(didAbort).toBe(false); aborty.abort(); - await Bun.sleep(2); + await Bun.sleep(20); return new Response("Test failed!"); }, }, async server => { - expect(async () => fetch(`http://${server.hostname}:${server.port}`, { signal: aborty.signal })).toThrow( - "The operation was aborted.", - ); + expect(async () => fetch(server.url.origin, { signal: aborty.signal })).toThrow("The operation was aborted."); - await Bun.sleep(1); + await Bun.sleep(10); expect(didAbort).toBe(true); }, @@ -173,7 +249,7 @@ it("should work for a file", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -210,7 +286,7 @@ it("request.url should be based on the Host header", async () => { }, }, async server => { - const expected = `http://${server.hostname}:${server.port}/helloooo`; + const expected = `${server.url.origin}/helloooo`; const response = await fetch(expected, { headers: { Host: "example.com", @@ -250,7 +326,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(402); expect(response.headers.get("I-AM")).toBe("A-TEAPOT"); expect(await response.text()).toBe(""); @@ -265,12 +341,10 @@ describe("streaming", () => { await runTest( { error(e) { - console.log("test case error()"); pass = false; return new Response("FAIL", { status: 555 }); }, fetch(req) { - console.log("test case fetch()"); const stream = new ReadableStream({ async pull(controller) { controller.enqueue("PASS"); @@ -278,32 +352,26 @@ describe("streaming", () => { throw new Error("FAIL"); }, }); - console.log("after constructing ReadableStream"); const r = new Response(stream, options); - console.log("after constructing Response"); return r; }, }, async server => { - console.log("async server() => {}"); - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); // connection terminated expect(await response.text()).toBe(""); expect(response.status).toBe(options.status ?? 200); expect(pass).toBe(true); - console.log("done test A"); }, ); } it("with headers", async () => { - console.log("with headers before anything"); await execute({ headers: { "X-A": "123", }, }); - console.log("with headers after everything"); }); it("with headers and status", async () => { @@ -344,7 +412,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); const text = await response.text(); expect(text.length).toBe(textToExpect.length); expect(text).toBe(textToExpect); @@ -369,7 +437,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -396,7 +464,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(200); expect(await response.text()).toBe("Test Passed"); }, @@ -427,7 +495,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(500); }, ); @@ -454,7 +522,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(500); expect(await response.text()).toBe("Fail"); expect(pass).toBe(true); @@ -485,7 +553,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -508,7 +576,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); const text = await response.text(); expect(text).toBe(textToExpect); }, @@ -536,7 +604,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -572,7 +640,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); count++; }, @@ -601,7 +669,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -631,7 +699,7 @@ describe("streaming", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -646,7 +714,7 @@ it("should work for a hello world", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe("Hello, world!"); }, ); @@ -662,7 +730,7 @@ it("should work for a blob", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -678,7 +746,7 @@ it("should work for a blob stream", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -694,7 +762,7 @@ it("should work for a file stream", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); @@ -714,7 +782,7 @@ it("fetch should work with headers", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`, { + const response = await fetch(server.url.origin, { headers: { "X-Foo": "bar", }, @@ -736,7 +804,7 @@ it(`should work for a file ${count} times serial`, async () => { }, async server => { for (let i = 0; i < count; i++) { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); } }, @@ -753,7 +821,7 @@ it(`should work for ArrayBuffer ${count} times serial`, async () => { }, async server => { for (let i = 0; i < count; i++) { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); } }, @@ -772,11 +840,11 @@ describe("parallel", () => { async server => { for (let i = 0; i < count; ) { let responses = await Promise.all([ - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), ]); for (let response of responses) { @@ -798,11 +866,11 @@ describe("parallel", () => { async server => { for (let i = 0; i < count; ) { let responses = await Promise.all([ - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), - fetch(`http://${server.hostname}:${server.port}`), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), + fetch(server.url.origin), ]); for (let response of responses) { @@ -823,10 +891,10 @@ it("should support reloading", async () => { fetch: first, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(await response.text()).toBe("first"); server.reload({ fetch: second }); - const response2 = await fetch(`http://${server.hostname}:${server.port}`); + const response2 = await fetch(server.url.origin); expect(await response2.text()).toBe("second"); }, ); @@ -904,7 +972,7 @@ describe("status code text", () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.status).toBe(parseInt(code)); expect(response.statusText).toBe(fixture[code]); }, @@ -927,7 +995,7 @@ it("should support multiple Set-Cookie headers", async () => { }, }, async server => { - const response = await fetch(`http://${server.hostname}:${server.port}`); + const response = await fetch(server.url.origin); expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); expect(response.headers.get("Set-Cookie")).toEqual("foo=bar, baz=qux"); @@ -995,7 +1063,7 @@ describe("should support Content-Range with Bun.file()", () => { for (const [start, end] of good) { it(`good range: ${start} - ${end}`, async () => { await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`, { + const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`, { verbose: true, }); expect(await response.arrayBuffer()).toEqual(full.buffer.slice(start, end)); @@ -1007,7 +1075,7 @@ describe("should support Content-Range with Bun.file()", () => { for (const [start, end] of good) { it(`good range with size: ${start} - ${end}`, async () => { await getServerWithSize(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`, { + const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`, { verbose: true, }); expect(parseInt(response.headers.get("Content-Range")?.split("/")[1])).toEqual(full.byteLength); @@ -1032,7 +1100,7 @@ describe("should support Content-Range with Bun.file()", () => { for (const [start, end] of emptyRanges) { it(`empty range: ${start} - ${end}`, async () => { await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`); + const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`); const out = await response.arrayBuffer(); expect(out).toEqual(new ArrayBuffer(0)); expect(response.status).toBe(206); @@ -1054,7 +1122,7 @@ describe("should support Content-Range with Bun.file()", () => { for (const [start, end] of badRanges) { it(`bad range: ${start} - ${end}`, async () => { await getServer(async server => { - const response = await fetch(`http://${server.hostname}:${server.port}/?start=${start}&end=${end}`); + const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`); const out = await response.arrayBuffer(); expect(out).toEqual(new ArrayBuffer(0)); expect(response.status).toBe(206); @@ -1097,7 +1165,7 @@ it("request body and signal life cycle", async () => { const requests = []; for (let j = 0; j < 10; j++) { for (let i = 0; i < 250; i++) { - requests.push(fetch(`http://${server.hostname}:${server.port}`)); + requests.push(fetch(server.url.origin)); } await Promise.all(requests); @@ -1130,7 +1198,7 @@ it("propagates content-type from a Bun.file()'s file path in fetch()", async () }); // @ts-ignore - const reqBody = new Request(`http://${server.hostname}:${server.port}`, { + const reqBody = new Request(server.url.origin, { body, method: "POST", }); @@ -1155,7 +1223,7 @@ it("does propagate type for Blob", async () => { const body = new Blob(["hey"], { type: "text/plain;charset=utf-8" }); // @ts-ignore - const res = await fetch(`http://${server.hostname}:${server.port}`, { + const res = await fetch(server.url.origin, { body, method: "POST", }); @@ -1221,7 +1289,7 @@ it("#5859 text", async () => { }, }); - const response = await fetch(`http://${server.hostname}:${server.port}`, { + const response = await fetch(server.url.origin, { method: "POST", body: new Uint8Array([0xfd]), }); @@ -1244,7 +1312,7 @@ it("#5859 json", async () => { }, }); - const response = await fetch(`http://${server.hostname}:${server.port}`, { + const response = await fetch(server.url.origin, { method: "POST", body: new Uint8Array([0xfd]), }); @@ -1268,7 +1336,7 @@ it("server.requestIP (v4)", async () => { hostname: "127.0.0.1", }); - const response = await fetch(`http://${server.hostname}:${server.port}`).then(x => x.json()); + const response = await fetch(server.url.origin).then(x => x.json()); expect(response).toEqual({ address: "127.0.0.1", family: "IPv4", @@ -1333,7 +1401,7 @@ it("should response with HTTP 413 when request body is larger than maxRequestBod }); { - const resp = await fetch(`http://${server.hostname}:${server.port}`, { + const resp = await fetch(server.url.origin, { method: "POST", body: "A".repeat(10), }); @@ -1341,7 +1409,7 @@ it("should response with HTTP 413 when request body is larger than maxRequestBod expect(await resp.text()).toBe("OK"); } { - const resp = await fetch(`http://${server.hostname}:${server.port}`, { + const resp = await fetch(server.url.origin, { method: "POST", body: "A".repeat(11), }); @@ -1379,24 +1447,24 @@ it("should support promise returned from error", async () => { }); { - const resp = await fetch(`http://${server.hostname}:${server.port}/async-fulfilled`); + const resp = await fetch(`${server.url.origin}/async-fulfilled`); expect(resp.status).toBe(200); expect(await resp.text()).toBe("OK"); } { - const resp = await fetch(`http://${server.hostname}:${server.port}/async-pending`); + const resp = await fetch(`${server.url.origin}/async-pending`); expect(resp.status).toBe(200); expect(await resp.text()).toBe("OK"); } { - const resp = await fetch(`http://${server.hostname}:${server.port}/async-rejected`); + const resp = await fetch(`${server.url.origin}/async-rejected`); expect(resp.status).toBe(500); } { - const resp = await fetch(`http://${server.hostname}:${server.port}/async-rejected-pending`); + const resp = await fetch(`${server.url.origin}/async-rejected-pending`); expect(resp.status).toBe(500); } diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index 33c5c61d415c39..7ab28c887ca862 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -1,5 +1,5 @@ import fs, { mkdirSync } from "fs"; -import { it, expect, describe } from "bun:test"; +import { it, expect, describe, test } from "bun:test"; import path, { join } from "path"; import { gcTick, withoutAggressiveGC, bunExe, bunEnv, isWindows } from "harness"; import { tmpdir } from "os"; @@ -479,3 +479,20 @@ describe("ENOENT", () => { }); }); }); + +test("timed output should work", async () => { + const producer_file = path.join(import.meta.dir, "timed-stderr-output.js"); + + const producer = Bun.spawn([bunExe(), "run", producer_file], { + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + }); + + let text = ""; + for await (const chunk of producer.stderr) { + text += [...chunk].map(x => String.fromCharCode(x)).join(""); + await Bun.sleep(1000); + } + expect(text).toBe("0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n"); +}); diff --git a/test/js/bun/io/timed-stderr-output.js b/test/js/bun/io/timed-stderr-output.js new file mode 100644 index 00000000000000..3a3e9892f87a69 --- /dev/null +++ b/test/js/bun/io/timed-stderr-output.js @@ -0,0 +1,4 @@ +for (let i = 0; i <= 25; i++) { + await Bun.write(Bun.stderr, i + "\n"); + await Bun.sleep(100); +} diff --git a/test/js/bun/net/socket-huge-fixture.js b/test/js/bun/net/socket-huge-fixture.js index e8fc9653cd4089..bee9756f27837f 100644 --- a/test/js/bun/net/socket-huge-fixture.js +++ b/test/js/bun/net/socket-huge-fixture.js @@ -1,19 +1,7 @@ import { connect, listen } from "bun"; +import { fillRepeating } from "harness"; const huge = Buffer.alloc(1024 * 1024 * 1024); -export function fillRepeating(dstBuffer, start, end) { - let len = dstBuffer.length, // important: use indices length, not byte-length - sLen = end - start, - p = sLen; // set initial position = source sequence length - - // step 2: copy existing data doubling segment length per iteration - while (p < len) { - if (p + sLen > len) sLen = len - p; // if not power of 2, truncate last segment - dstBuffer.copyWithin(p, start, sLen); // internal copy - p += sLen; // add current length to offset - sLen <<= 1; // double length for next segment - } -} for (let i = 0; i < 1024; i++) { huge[i] = (Math.random() * 255) | 0; } diff --git a/test/js/bun/shell/bunshell-instance.test.ts b/test/js/bun/shell/bunshell-instance.test.ts index 17a4b571309344..496f336132c642 100644 --- a/test/js/bun/shell/bunshell-instance.test.ts +++ b/test/js/bun/shell/bunshell-instance.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" import { test, expect, describe } from "bun:test"; import { $ } from "bun"; diff --git a/test/js/bun/shell/bunshell.test.ts b/test/js/bun/shell/bunshell.test.ts index 963c824a800f62..a6d7067eddf26b 100644 --- a/test/js/bun/shell/bunshell.test.ts +++ b/test/js/bun/shell/bunshell.test.ts @@ -10,7 +10,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, realpath, rm } from "fs/promises"; import { bunEnv, runWithErrorPromise, tempDirWithFiles } from "harness"; import { tmpdir } from "os"; -import { join } from "path"; +import { join, sep } from "path"; import { TestBuilder, sortedShellOutput } from "./util"; $.env(bunEnv); @@ -38,6 +38,15 @@ afterAll(async () => { const BUN = process.argv0; describe("bunshell", () => { + describe("concurrency", () => { + test("writing to stdout", async () => { + await Promise.all([ + TestBuilder.command`echo 1`.stdout("1\n").run(), + TestBuilder.command`echo 2`.stdout("2\n").run(), + TestBuilder.command`echo 3`.stdout("3\n").run(), + ]); + }); + }); test("js_obj_test", async () => { function runTest(name: string, builder: TestBuilder) { test(`js_obj_test_name_${name}`, async () => { @@ -72,24 +81,25 @@ describe("bunshell", () => { `"\\"hello\\" \\"lol\\" \\"nice\\"lkasjf;jdfla<>SKDJFLKSF"`, ); - test("wrapped in quotes", async () => { + describe("wrapped in quotes", async () => { const url = "http://www.example.com?candy_name=M&M"; - await TestBuilder.command`echo url="${url}"`.stdout(`url=${url}\n`).run(); - await TestBuilder.command`echo url='${url}'`.stdout(`url=${url}\n`).run(); - await TestBuilder.command`echo url=${url}`.stdout(`url=${url}\n`).run(); + TestBuilder.command`echo url="${url}"`.stdout(`url=${url}\n`).runAsTest("double quotes"); + TestBuilder.command`echo url='${url}'`.stdout(`url=${url}\n`).runAsTest("single quotes"); + TestBuilder.command`echo url=${url}`.stdout(`url=${url}\n`).runAsTest("no quotes"); }); - test("escape var", async () => { + describe("escape var", async () => { const shellvar = "$FOO"; - await TestBuilder.command`FOO=bar && echo "${shellvar}"`.stdout(`$FOO\n`).run(); - await TestBuilder.command`FOO=bar && echo '${shellvar}'`.stdout(`$FOO\n`).run(); - await TestBuilder.command`FOO=bar && echo ${shellvar}`.stdout(`$FOO\n`).run(); + TestBuilder.command`FOO=bar && echo "${shellvar}"`.stdout(`$FOO\n`).runAsTest("double quotes"); + TestBuilder.command`FOO=bar && echo '${shellvar}'`.stdout(`$FOO\n`).runAsTest("single quotes"); + TestBuilder.command`FOO=bar && echo ${shellvar}`.stdout(`$FOO\n`).runAsTest("no quotes"); }); test("can't escape a js string/obj ref", async () => { const shellvar = "$FOO"; await TestBuilder.command`FOO=bar && echo \\${shellvar}`.stdout(`\\$FOO\n`).run(); const buf = new Uint8Array(1); + expect(async () => { await TestBuilder.command`echo hi > \\${buf}`.run(); }).toThrow("Redirection with no file"); @@ -107,7 +117,7 @@ describe("bunshell", () => { }); describe("quiet", async () => { - test("basic", async () => { + test.todo("basic", async () => { // Check its buffered { const { stdout, stderr } = await $`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e "console.log('hi'); console.error('lol')"`; @@ -177,21 +187,23 @@ describe("bunshell", () => { // Issue: #8982 // https://github.com/oven-sh/bun/issues/8982 - test("word splitting", async () => { - await TestBuilder.command`echo $(echo id)/$(echo region)`.stdout("id/region\n").run(); - await TestBuilder.command`echo $(echo hi id)/$(echo region)`.stdout("hi id/region\n").run(); + describe("word splitting", async () => { + TestBuilder.command`echo $(echo id)/$(echo region)`.stdout("id/region\n").runAsTest("concatenated cmd substs"); + TestBuilder.command`echo $(echo hi id)/$(echo region)` + .stdout("hi id/region\n") + .runAsTest("cmd subst with whitespace gets split"); // Make sure its one whole argument - await TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo id)/$(echo region)` + TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo id)/$(echo region)` .stdout('["id/region"]\n') .ensureTempDir() - .run(); + .runAsTest("make sure its one whole argument"); // Make sure its two separate arguments - await TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo hi id)/$(echo region)` + TestBuilder.command`echo {"console.log(JSON.stringify(process.argv.slice(2)))"} > temp_script.ts; BUN_DEBUG_QUIET_LOGS=1 ${BUN} run temp_script.ts $(echo hi id)/$(echo region)` .stdout('["hi","id/region"]\n') .ensureTempDir() - .run(); + .runAsTest("make sure its two separate arguments"); }); }); @@ -226,12 +238,12 @@ describe("bunshell", () => { }); test("var value", async () => { - const error = runWithErrorPromise(async () => { + const error = await runWithErrorPromise(async () => { const whatsupbro = "元気かい、兄弟"; const { stdout } = await $`FOO=${whatsupbro}; echo $FOO`; expect(stdout.toString("utf-8")).toEqual(whatsupbro + "\n"); }); - expect(error).toBeDefined(); + expect(error).toBeUndefined(); }); test("in compound word", async () => { @@ -270,11 +282,11 @@ describe("bunshell", () => { }); describe("latin-1", async () => { - test("basic", async () => { - await TestBuilder.command`echo ${"à"}`.stdout("à\n").run(); - await TestBuilder.command`echo ${" à"}`.stdout(" à\n").run(); - await TestBuilder.command`echo ${"à¿"}`.stdout("à¿\n").run(); - await TestBuilder.command`echo ${'"à¿"'}`.stdout('"à¿"\n').run(); + describe("basic", async () => { + TestBuilder.command`echo ${"à"}`.stdout("à\n").runAsTest("lone latin-1 character"); + TestBuilder.command`echo ${" à"}`.stdout(" à\n").runAsTest("latin-1 character preceded by space"); + TestBuilder.command`echo ${"à¿"}`.stdout("à¿\n").runAsTest("multiple latin-1 characters"); + TestBuilder.command`echo ${'"à¿"'}`.stdout('"à¿"\n').runAsTest("latin-1 characters in quotes"); }); }); @@ -416,8 +428,8 @@ describe("bunshell", () => { }); test("export var", async () => { - const buffer = Buffer.alloc(8192); - const buffer2 = Buffer.alloc(8192); + const buffer = Buffer.alloc(1 << 20); + const buffer2 = Buffer.alloc(1 << 20); await $`export FOO=bar && BAZ=1 ${BUN} -e "console.log(JSON.stringify(process.env))" > ${buffer} && BUN_TEST_VAR=1 ${BUN} -e "console.log(JSON.stringify(process.env))" > ${buffer2}`; const str1 = stringifyBuffer(buffer); @@ -437,7 +449,7 @@ describe("bunshell", () => { }); test("syntax edgecase", async () => { - const buffer = new Uint8Array(8192); + const buffer = new Uint8Array(1 << 20); const shellProc = await $`FOO=bar BUN_TEST_VAR=1 ${BUN} -e "console.log(JSON.stringify(process.env))"> ${buffer}`; const str = stringifyBuffer(buffer); @@ -501,11 +513,11 @@ describe("bunshell", () => { .filter(s => s.length !== 0) .sort(), ).toEqual( - `${temp_dir}/foo -${temp_dir}/dir/files -${temp_dir}/dir/some -${temp_dir}/dir -${temp_dir}/bar + `${join(temp_dir, "foo")} +${join(temp_dir, "dir", "files")} +${join(temp_dir, "dir", "some")} +${join(temp_dir, "dir")} +${join(temp_dir, "bar")} ${temp_dir}` .split("\n") .sort(), @@ -520,142 +532,148 @@ ${temp_dir}` }); describe("deno_task", () => { - test("commands", async () => { - await TestBuilder.command`echo 1`.stdout("1\n").run(); - await TestBuilder.command`echo 1 2 3`.stdout("1 2 3\n").run(); - await TestBuilder.command`echo "1 2 3"`.stdout("1 2 3\n").run(); - await TestBuilder.command`echo 1 2\ \ \ 3`.stdout("1 2 3\n").run(); - await TestBuilder.command`echo "1 2\ \ \ 3"`.stdout("1 2\\ \\ \\ 3\n").run(); - await TestBuilder.command`echo test$(echo 1 2)`.stdout("test1 2\n").run(); - await TestBuilder.command`echo test$(echo "1 2")`.stdout("test1 2\n").run(); - await TestBuilder.command`echo "test$(echo "1 2")"`.stdout("test1 2\n").run(); - await TestBuilder.command`echo test$(echo "1 2 3")`.stdout("test1 2 3\n").run(); - await TestBuilder.command`VAR=1 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)' && echo $VAR` + describe("commands", async () => { + TestBuilder.command`echo 1`.stdout("1\n").runAsTest("echo 1"); + TestBuilder.command`echo 1 2 3`.stdout("1 2 3\n").runAsTest("echo 1 2 3"); + TestBuilder.command`echo "1 2 3"`.stdout("1 2 3\n").runAsTest('echo "1 2 3"'); + TestBuilder.command`echo 1 2\ \ \ 3`.stdout("1 2 3\n").runAsTest("echo 1 2\\ \\ \\ 3"); + TestBuilder.command`echo "1 2\ \ \ 3"`.stdout("1 2\\ \\ \\ 3\n").runAsTest('echo "1 2\\ \\ \\ 3"'); + TestBuilder.command`echo test$(echo 1 2)`.stdout("test1 2\n").runAsTest("echo test$(echo 1 2)"); + TestBuilder.command`echo test$(echo "1 2")`.stdout("test1 2\n").runAsTest('echo test$(echo "1 2")'); + TestBuilder.command`echo "test$(echo "1 2")"`.stdout("test1 2\n").runAsTest('echo "test$(echo "1 2")"'); + TestBuilder.command`echo test$(echo "1 2 3")`.stdout("test1 2 3\n").runAsTest('echo test$(echo "1 2 3")'); + TestBuilder.command`VAR=1 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)' && echo $VAR` .stdout("1\n\n") - .run(); - await TestBuilder.command`VAR=1 VAR2=2 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR + process.env.VAR2)'` + .runAsTest("shell var in command"); + TestBuilder.command`VAR=1 VAR2=2 BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR + process.env.VAR2)'` .stdout("12\n") - .run(); - await TestBuilder.command`EMPTY= BUN_TEST_VAR=1 ${BUN} -e ${"console.log(`EMPTY: ${process.env.EMPTY}`)"}` + .runAsTest("shell var in command 2"); + TestBuilder.command`EMPTY= BUN_TEST_VAR=1 ${BUN} -e ${"console.log(`EMPTY: ${process.env.EMPTY}`)"}` .stdout("EMPTY: \n") - .run(); - await TestBuilder.command`"echo" "1"`.stdout("1\n").run(); - await TestBuilder.command`echo test-dashes`.stdout("test-dashes\n").run(); - await TestBuilder.command`echo 'a/b'/c`.stdout("a/b/c\n").run(); - await TestBuilder.command`echo 'a/b'ctest\"te st\"'asdf'`.stdout('a/bctest"te st"asdf\n').run(); - await TestBuilder.command`echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'` + .runAsTest("empty shell var"); + TestBuilder.command`"echo" "1"`.stdout("1\n").runAsTest("echo 1 quoted"); + TestBuilder.command`echo test-dashes`.stdout("test-dashes\n").runAsTest("echo test-dashes"); + TestBuilder.command`echo 'a/b'/c`.stdout("a/b/c\n").runAsTest("echo 'a/b'/c"); + TestBuilder.command`echo 'a/b'ctest\"te st\"'asdf'` + .stdout('a/bctest"te st"asdf\n') + .runAsTest("echoing a bunch of escapes and quotations"); + TestBuilder.command`echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'` .stdout(`--test="2" --test=2 test"TEST" TESTtestTEST testtest testtest"test" "test""test"test\n`) - .run(); + .runAsTest("echoing a bunch of escapes and quotations 2"); }); - test("boolean logic", async () => { - await TestBuilder.command`echo 1 && echo 2 || echo 3`.stdout("1\n2\n").run(); - await TestBuilder.command`echo 1 || echo 2 && echo 3`.stdout("1\n3\n").run(); + describe("boolean logic", async () => { + TestBuilder.command`echo 1 && echo 2 || echo 3`.stdout("1\n2\n").runAsTest("echo 1 && echo 2 || echo 3"); + TestBuilder.command`echo 1 || echo 2 && echo 3`.stdout("1\n3\n").runAsTest("echo 1 || echo 2 && echo 3"); - await TestBuilder.command`echo 1 || (echo 2 && echo 3)`.error(TestBuilder.UNEXPECTED_SUBSHELL_ERROR_OPEN).run(); - await TestBuilder.command`false || false || (echo 2 && false) || echo 3` + TestBuilder.command`echo 1 || (echo 2 && echo 3)` .error(TestBuilder.UNEXPECTED_SUBSHELL_ERROR_OPEN) - .run(); + .runAsTest("or with subshell"); + TestBuilder.command`false || false || (echo 2 && false) || echo 3` + .error(TestBuilder.UNEXPECTED_SUBSHELL_ERROR_OPEN) + .runAsTest("or with subshell 2"); // await TestBuilder.command`echo 1 || (echo 2 && echo 3)`.stdout("1\n").run(); // await TestBuilder.command`false || false || (echo 2 && false) || echo 3`.stdout("2\n3\n").run(); }); - test("command substitution", async () => { - await TestBuilder.command`echo $(echo 1)`.stdout("1\n").run(); - await TestBuilder.command`echo $(echo 1 && echo 2)`.stdout("1 2\n").run(); + describe("command substitution", async () => { + TestBuilder.command`echo $(echo 1)`.stdout("1\n").runAsTest("nested echo cmd subst"); + TestBuilder.command`echo $(echo 1 && echo 2)`.stdout("1 2\n").runAsTest("nested echo cmd subst with conditional"); // TODO Sleep tests }); - test("shell variables", async () => { - await TestBuilder.command`echo $VAR && VAR=1 && echo $VAR && ${BUN} -e ${"console.log(process.env.VAR)"}` + describe("shell variables", async () => { + TestBuilder.command`echo $VAR && VAR=1 && echo $VAR && ${BUN} -e ${"console.log(process.env.VAR)"}` .stdout("\n1\nundefined\n") - .run(); + .runAsTest("shell var"); - await TestBuilder.command`VAR=1 && echo $VAR$VAR`.stdout("11\n").run(); + TestBuilder.command`VAR=1 && echo $VAR$VAR`.stdout("11\n").runAsTest("shell var 2"); - await TestBuilder.command`VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR) ; echo $ ; echo \$VAR` + TestBuilder.command`VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR) ; echo $ ; echo \$VAR` .stdout("Test1\nTest: 1\nCommandSub\n$\n$VAR\n") .stderr("bun: command not found: 1\n") - .run(); + .runAsTest("shell var 3"); }); - test("env variables", async () => { - await TestBuilder.command`echo $VAR && export VAR=1 && echo $VAR && BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)'` + describe("env variables", async () => { + TestBuilder.command`echo $VAR && export VAR=1 && echo $VAR && BUN_TEST_VAR=1 ${BUN} -e 'console.log(process.env.VAR)'` .stdout("\n1\n1\n") - .run(); + .runAsTest("exported vars"); - await TestBuilder.command`export VAR=1 VAR2=testing VAR3="test this out" && echo $VAR $VAR2 $VAR3` + TestBuilder.command`export VAR=1 VAR2=testing VAR3="test this out" && echo $VAR $VAR2 $VAR3` .stdout("1 testing test this out\n") - .run(); + .runAsTest("exported vars 2"); }); - test("pipeline", async () => { - await TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + describe("pipeline", async () => { + TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` .stdout("1\n") - .run(); + .runAsTest("basic pipe"); - await TestBuilder.command`echo 1 | echo 2 && echo 3`.stdout("2\n3\n").run(); + TestBuilder.command`echo 1 | echo 2 && echo 3`.stdout("2\n3\n").runAsTest("pipe in conditional"); // await TestBuilder.command`echo $(sleep 0.1 && echo 2 & echo 1) | BUN_TEST_VAR=1 ${BUN} -e 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'` // .stdout("1 2\n") // .run(); - await TestBuilder.command`echo 2 | echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + TestBuilder.command`echo 2 | echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` .stdout("1\n") - .run(); + .runAsTest("multi pipe"); - await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` .stdout("1\n") .stderr("2\n") - .run(); + .runAsTest("piping subprocesses"); - await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` // .stdout("1\n2\n") .error("Piping stdout and stderr (`|&`) is not supported yet. Please file an issue on GitHub.") - .run(); + .runAsTest("|&"); // await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(2);' | BUN_TEST_VAR=1 ${BUN} -e 'setTimeout(async () => { await Deno.stdin.readable.pipeTo(Deno.stderr.writable) }, 10)' |& BUN_TEST_VAR=1 ${BUN} -e 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)'` // .stderr("2\n1\n") // .run(); - await TestBuilder.command`echo 1 |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` + TestBuilder.command`echo 1 |& BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)'` // .stdout("1\n") .error("Piping stdout and stderr (`|&`) is not supported yet. Please file an issue on GitHub.") - .run(); + .runAsTest("|& 2"); - await TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)' > output.txt` + TestBuilder.command`echo 1 | BUN_DEBUG_QUIET_LOGS=1 BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stdout)' > output.txt` .fileEquals("output.txt", "1\n") - .run(); + .runAsTest("pipe with redirect to file"); - await TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stderr)' 2> output.txt` + TestBuilder.command`echo 1 | BUN_TEST_VAR=1 ${BUN} -e 'process.stdin.pipe(process.stderr)' 2> output.txt` .fileEquals("output.txt", "1\n") - .run(); + .runAsTest("pipe with redirect stderr to file"); }); - test("redirects", async function igodf() { + describe("redirects", async function igodf() { // await TestBuilder.command`echo 5 6 7 > test.txt`.fileEquals("test.txt", "5 6 7\n").run(); // await TestBuilder.command`echo 1 2 3 && echo 1 > test.txt`.stdout("1 2 3\n").fileEquals("test.txt", "1\n").run(); // subdir - await TestBuilder.command`mkdir subdir && cd subdir && echo 1 2 3 > test.txt` + TestBuilder.command`mkdir subdir && cd subdir && echo 1 2 3 > test.txt` .fileEquals(`subdir/test.txt`, "1 2 3\n") - .run(); + .runAsTest("redirect to file"); // absolute path - await TestBuilder.command`echo 1 2 3 > "$PWD/test.txt"`.fileEquals("test.txt", "1 2 3\n").run(); + TestBuilder.command`echo 1 2 3 > "$PWD/test.txt"` + .fileEquals("test.txt", "1 2 3\n") + .runAsTest("redirection path gets expanded"); // stdout - await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 1> test.txt` + TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 1> test.txt` .stderr("5\n") .fileEquals("test.txt", "1\n") - .run(); + .runAsTest("redirect stdout of subproccess"); // stderr - await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> test.txt` + TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> test.txt` .stdout("1\n") .fileEquals("test.txt", "5\n") - .run(); + .runAsTest("redirect stderr of subprocess"); // invalid fd // await TestBuilder.command`echo 2 3> test.txt` @@ -665,17 +683,19 @@ describe("deno_task", () => { // .run(); // /dev/null - await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> /dev/null` + TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); console.error(5)' 2> /dev/null` .stdout("1\n") - .run(); + .runAsTest("/dev/null"); // appending - await TestBuilder.command`echo 1 > test.txt && echo 2 >> test.txt`.fileEquals("test.txt", "1\n2\n").run(); + TestBuilder.command`echo 1 > test.txt && echo 2 >> test.txt` + .fileEquals("test.txt", "1\n2\n") + .runAsTest("appending"); // &> and &>> redirect await TestBuilder.command`BUN_TEST_VAR=1 ${BUN} -e 'console.log(1); setTimeout(() => console.error(23), 10)' &> file.txt && BUN_TEST_VAR=1 ${BUN} -e 'console.log(456); setTimeout(() => console.error(789), 10)' &>> file.txt` .fileEquals("file.txt", "1\n23\n456\n789\n") - .run(); + .runAsTest("&> and &>> redirect"); // multiple arguments after re-direct // await TestBuilder.command`export TwoArgs=testing\\ this && echo 1 > $TwoArgs` @@ -686,36 +706,42 @@ describe("deno_task", () => { // .run(); // zero arguments after re-direct - await TestBuilder.command`echo 1 > $EMPTY`.stderr("bun: ambiguous redirect: at `echo`\n").exitCode(1).run(); + TestBuilder.command`echo 1 > $EMPTY` + .stderr("bun: ambiguous redirect: at `echo`\n") + .exitCode(1) + .runAsTest("zero arguments after re-direct"); - await TestBuilder.command`echo foo bar > file.txt; cat < file.txt`.ensureTempDir().stdout("foo bar\n").run(); + TestBuilder.command`echo foo bar > file.txt; cat < file.txt` + .ensureTempDir() + .stdout("foo bar\n") + .runAsTest("redirect input"); - await TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1` + TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1` .stdout("Stdout\nStderr\n") - .run(); + .runAsTest("redirect stderr to stdout"); - await TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2` + TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2` .stderr("Stdout\nStderr\n") - .run(); + .runAsTest("redirect stdout to stderr"); - await TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1` + TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 2>&1` .stdout("Stdout\nStderr\n") .quiet() - .run(); + .runAsTest("redirect stderr to stdout quiet"); - await TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2` + TestBuilder.command`BUN_DEBUG_QUIET_LOGS=1 ${BUN} -e ${"console.log('Stdout'); console.error('Stderr')"} 1>&2` .stderr("Stdout\nStderr\n") .quiet() - .run(); + .runAsTest("redirect stdout to stderr quiet"); }); - test("pwd", async () => { - await TestBuilder.command`pwd && cd sub_dir && pwd && cd ../ && pwd` + describe("pwd", async () => { + TestBuilder.command`pwd && cd sub_dir && pwd && cd ../ && pwd` .directory("sub_dir") .file("file.txt", "test") // $TEMP_DIR gets replaced with the actual temp dir by the test runner - .stdout(`$TEMP_DIR\n$TEMP_DIR/sub_dir\n$TEMP_DIR\n`) - .run(); + .stdout(`$TEMP_DIR\n${join("$TEMP_DIR", "sub_dir")}\n$TEMP_DIR\n`) + .runAsTest("pwd"); }); test("change env", async () => { diff --git a/test/js/bun/shell/commands/rm.test.ts b/test/js/bun/shell/commands/rm.test.ts index e9e38da2b9b0df..7272fa6788a8c4 100644 --- a/test/js/bun/shell/commands/rm.test.ts +++ b/test/js/bun/shell/commands/rm.test.ts @@ -14,11 +14,19 @@ import { ShellOutput } from "bun"; import { TestBuilder, sortedShellOutput } from "../util"; const fileExists = async (path: string): Promise => - $`ls -d ${path}`.then(o => o.stdout.toString() == `${path}\n`); + $`ls -d ${path}`.then(o => o.stdout.toString() === `${path}\n`); $.nothrow(); +const BUN = process.argv0; +const DEV_NULL = process.platform === "win32" ? "NUL" : "/dev/null"; + describe("bunshell rm", () => { + TestBuilder.command`echo ${packagejson()} > package.json; ${BUN} install &> ${DEV_NULL}; rm -rf node_modules/` + .ensureTempDir() + .doesNotExist("node_modules") + .runAsTest("node_modules"); + test("force", async () => { const files = { "existent.txt": "", @@ -134,3 +142,29 @@ foo/ } }); }); + +function packagejson() { + return `{ + "name": "dummy", + "dependencies": { + "@biomejs/biome": "^1.5.3", + "@vscode/debugadapter": "^1.61.0", + "esbuild": "^0.17.15", + "eslint": "^8.20.0", + "eslint-config-prettier": "^8.5.0", + "mitata": "^0.1.3", + "peechy": "0.4.34", + "prettier": "3.2.2", + "react": "next", + "react-dom": "next", + "source-map-js": "^1.0.2", + "typescript": "^5.0.2" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@typescript-eslint/eslint-plugin": "^5.31.0", + "@typescript-eslint/parser": "^5.31.0" + }, + "version": "0.0.0" +}`; +} diff --git a/test/js/bun/shell/lazy.test.ts b/test/js/bun/shell/lazy.test.ts index 0ebf54c66d4f90..20dd17382e0c51 100644 --- a/test/js/bun/shell/lazy.test.ts +++ b/test/js/bun/shell/lazy.test.ts @@ -1,5 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" - import { $ } from "bun"; import { test, expect } from "bun:test"; import { tempDirWithFiles } from "harness"; diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index 802d14d4a0a4ad..a90f0660059e60 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -54,10 +54,7 @@ const TESTS: [name: string, builder: () => TestBuilder, runs?: number][] = [ describe("fd leak", () => { function fdLeakTest(name: string, builder: () => TestBuilder, runs: number = 500) { test(`fdleak_${name}`, async () => { - for (let i = 0; i < 5; i++) { - await builder().quiet().run(); - } - + Bun.gc(true); const baseline = openSync(devNull, "r"); closeSync(baseline); diff --git a/test/js/bun/shell/lex.test.ts b/test/js/bun/shell/lex.test.ts index 9f0e4856ef3e49..dc2274b1ab20a5 100644 --- a/test/js/bun/shell/lex.test.ts +++ b/test/js/bun/shell/lex.test.ts @@ -1,5 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" - import { $ } from "bun"; import { TestBuilder, redirect } from "./util"; diff --git a/test/js/bun/shell/test_builder.ts b/test/js/bun/shell/test_builder.ts index 9bb9911e0bfbec..269eab09977459 100644 --- a/test/js/bun/shell/test_builder.ts +++ b/test/js/bun/shell/test_builder.ts @@ -15,6 +15,7 @@ export class TestBuilder { private expected_exit_code: number = 0; private expected_error: ShellError | string | boolean | undefined = undefined; private file_equals: { [filename: string]: string } = {}; + private _doesNotExist: string[] = []; private tempdir: string | undefined = undefined; private _env: { [key: string]: string } | undefined = undefined; @@ -47,6 +48,11 @@ export class TestBuilder { return this; } + doesNotExist(path: string): this { + this._doesNotExist.push(path); + return this; + } + file(path: string, contents: string): this { const tempdir = this.getTempDir(); fs.writeFileSync(join(tempdir, path), contents); @@ -174,9 +180,21 @@ export class TestBuilder { expect(actual).toEqual(expected); } + for (const fsname of this._doesNotExist) { + expect(fs.existsSync(join(this.tempdir!, fsname))).toBeFalsy(); + } + // return output; } + runAsTest(name: string) { + // biome-ignore lint/complexity/noUselessThisAlias: + const tb = this; + test(name, async () => { + await tb.run(); + }); + } + // async run(): Promise { // async function doTest(tb: TestBuilder) { // if (tb.promise.type === "err") { diff --git a/test/js/bun/spawn/bash-echo.sh b/test/js/bun/spawn/bash-echo.sh index 57bca4b01669a8..96965d7a8e8829 100644 --- a/test/js/bun/spawn/bash-echo.sh +++ b/test/js/bun/spawn/bash-echo.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash -myvar=$(cat /dev/stdin) +# On Linux/Cygwin, $( { + const interval = setInterval(dumpStats, 1000).unref(); + const maxFD = openSync(devNull, "w"); - for (let i = 0; i < N; i++) { - var exited; - await (async function () { - const tmperr = join(tmpdir(), "stdin-repro-error.log." + i); - const proc = spawn({ - cmd: [bunExe(), import.meta.dir + "/stdin-repro.js"], - stdout: "pipe", - stdin: "pipe", - stderr: Bun.file(tmperr), - env: bunEnv, - }); - exited = proc.exited; - var counter = 0; - var inCounter = 0; - var chunks: any[] = []; - const prom = (async function () { - try { - for await (var chunk of proc.stdout) { - chunks.push(chunk); + var remaining = N; + while (remaining > 0) { + const proms = new Array(concurrency); + for (let i = 0; i < concurrency; i++) { + proms[i] = (async function () { + const proc = spawn({ + cmd: [bunExe(), join(import.meta.dir, "stdin-repro.js")], + stdout: "pipe", + stdin: "pipe", + stderr: "inherit", + env: { ...bunEnv }, + }); + + const prom2 = (async function () { + let inCounter = 0; + while (true) { + proc.stdin!.write("Wrote to stdin!\n"); + await proc.stdin!.flush(); + await Bun.sleep(delay); + + if (inCounter++ === 6) break; } - } catch (e: any) { - console.log(e.stack); - throw e; - } - })(); + await proc.stdin!.end(); + return inCounter; + })(); - const prom2 = (async function () { - while (true) { - proc.stdin!.write("Wrote to stdin!\n"); - inCounter++; - await new Promise(resolve => setTimeout(resolve, 8)); + const prom = (async function () { + let chunks: any[] = []; - if (inCounter === 4) break; - } - await proc.stdin!.end(); - })(); + try { + for await (var chunk of proc.stdout) { + chunks.push(chunk); + } + } catch (e: any) { + console.log(e.stack); + throw e; + } - await Promise.all([prom, prom2]); - expect(Buffer.concat(chunks).toString().trim()).toBe("Wrote to stdin!\n".repeat(4).trim()); - await proc.exited; + return Buffer.concat(chunks).toString().trim(); + })(); - try { - unlinkSync(tmperr); - } catch (e) {} - })(); + const [chunks, , exitCode] = await Promise.all([prom, prom2, proc.exited]); + + expect(chunks).toBe("Wrote to stdin!\n".repeat(7).trim()); + expect(exitCode).toBe(0); + })(); + } + await Promise.all(proms); + remaining -= concurrency; } closeSync(maxFD); @@ -64,4 +71,10 @@ test("spawn can write to stdin multiple chunks", async () => { // assert we didn't leak any file descriptors expect(newMaxFD).toBe(maxFD); -}, 20_000); + clearInterval(interval); + await expectMaxObjectTypeCount(expect, "ReadableStream", 10); + await expectMaxObjectTypeCount(expect, "ReadableStreamDefaultReader", 10); + await expectMaxObjectTypeCount(expect, "ReadableByteStreamController", 10); + await expectMaxObjectTypeCount(expect, "Subprocess", 5); + dumpStats(); +}, 60_000); diff --git a/test/js/bun/spawn/spawn-streaming-stdout.test.ts b/test/js/bun/spawn/spawn-streaming-stdout.test.ts index 7947ff9170f6c2..666482f74876ad 100644 --- a/test/js/bun/spawn/spawn-streaming-stdout.test.ts +++ b/test/js/bun/spawn/spawn-streaming-stdout.test.ts @@ -1,40 +1,46 @@ -// @known-failing-on-windows: 1 failing -import { it, test, expect } from "bun:test"; import { spawn } from "bun"; -import { bunExe, bunEnv, gcTick } from "harness"; +import { expect, test } from "bun:test"; import { closeSync, openSync } from "fs"; +import { bunEnv, bunExe, dumpStats, expectMaxObjectTypeCount, gcTick } from "harness"; import { devNull } from "os"; test("spawn can read from stdout multiple chunks", async () => { gcTick(true); var maxFD: number = -1; - for (let i = 0; i < 100; i++) { - await (async function () { - const proc = spawn({ - cmd: [bunExe(), import.meta.dir + "/spawn-streaming-stdout-repro.js"], - stdin: "ignore", - stdout: "pipe", - stderr: "ignore", - env: bunEnv, - }); - var chunks = []; - let counter = 0; - try { - for await (var chunk of proc.stdout) { - chunks.push(chunk); - counter++; - if (counter > 3) break; + let concurrency = 7; + const count = 100; + const interval = setInterval(dumpStats, 1000).unref(); + for (let i = 0; i < count; ) { + const promises = new Array(concurrency); + for (let j = 0; j < concurrency; j++) { + promises[j] = (async function () { + const proc = spawn({ + cmd: [bunExe(), import.meta.dir + "/spawn-streaming-stdout-repro.js"], + stdin: "ignore", + stdout: "pipe", + stderr: "ignore", + env: bunEnv, + }); + var chunks = []; + let counter = 0; + try { + for await (var chunk of proc.stdout) { + chunks.push(chunk); + counter++; + if (counter > 3) break; + } + } catch (e: any) { + console.log(e.stack); + throw e; } - } catch (e: any) { - console.log(e.stack); - throw e; - } - expect(counter).toBe(4); - // TODO: fix bug with returning SIGHUP instead of exit code 1 - proc.kill(); - expect(Buffer.concat(chunks).toString()).toBe("Wrote to stdout\n".repeat(4)); - await proc.exited; - })(); + expect(counter).toBe(4); + proc.kill(); + expect(Buffer.concat(chunks).toString()).toStartWith("Wrote to stdout\n".repeat(4)); + await proc.exited; + })(); + } + await Promise.all(promises); + i += concurrency; if (maxFD === -1) { maxFD = openSync(devNull, "w"); closeSync(maxFD); @@ -43,4 +49,10 @@ test("spawn can read from stdout multiple chunks", async () => { const newMaxFD = openSync(devNull, "w"); closeSync(newMaxFD); expect(newMaxFD).toBe(maxFD); -}, 60_000); + clearInterval(interval); + await expectMaxObjectTypeCount(expect, "ReadableStream", 10); + await expectMaxObjectTypeCount(expect, "ReadableStreamDefaultReader", 10); + await expectMaxObjectTypeCount(expect, "ReadableByteStreamController", 10); + await expectMaxObjectTypeCount(expect, "Subprocess", 5); + dumpStats(); +}, 60_0000); diff --git a/test/js/bun/spawn/spawn.ipc.test.ts b/test/js/bun/spawn/spawn.ipc.test.ts new file mode 100644 index 00000000000000..f492aa3ce1cc79 --- /dev/null +++ b/test/js/bun/spawn/spawn.ipc.test.ts @@ -0,0 +1,36 @@ +import { spawn } from "bun"; +import { describe, expect, it } from "bun:test"; +import { gcTick, bunExe } from "harness"; +import path from "path"; + +describe("ipc", () => { + it("the subprocess should be defined and the child should send", done => { + gcTick(); + const returned_subprocess = spawn([bunExe(), path.join(__dirname, "bun-ipc-child.js")], { + ipc: (message, subProcess) => { + expect(subProcess).toBe(returned_subprocess); + expect(message).toBe("hello"); + subProcess.kill(); + done(); + gcTick(); + }, + }); + }); + + it("the subprocess should receive the parent message and respond back", done => { + gcTick(); + + const parentMessage = "I am your father"; + const childProc = spawn([bunExe(), path.join(__dirname, "bun-ipc-child-respond.js")], { + ipc: (message, subProcess) => { + expect(message).toBe(`pong:${parentMessage}`); + subProcess.kill(); + done(); + gcTick(); + }, + }); + + childProc.send(parentMessage); + gcTick(); + }); +}); diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 860bb25fe2c35f..96cd78fdd7d894 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -1,9 +1,24 @@ -// @known-failing-on-windows: 1 failing import { ArrayBufferSink, readableStreamToText, spawn, spawnSync, write } from "bun"; -import { describe, expect, it } from "bun:test"; -import { gcTick as _gcTick, bunExe, bunEnv } from "harness"; -import { rmSync, writeFileSync } from "node:fs"; +import { beforeAll, describe, expect, it } from "bun:test"; +import { closeSync, fstatSync, openSync } from "fs"; +import { gcTick as _gcTick, bunEnv, bunExe, isWindows, withoutAggressiveGC } from "harness"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "path"; +let tmp; + +beforeAll(() => { + tmp = path.join(tmpdir(), "bun-spawn-" + Date.now().toString(32)) + path.sep; + rmSync(tmp, { force: true, recursive: true }); + mkdirSync(tmp, { recursive: true }); +}); + +function createHugeString() { + const buf = Buffer.allocUnsafe("hello".length * 100 * 500 + "hey".length); + buf.fill("hello"); + buf.write("hey", buf.length - "hey".length); + return buf.toString(); +} for (let [gcTick, label] of [ [_gcTick, "gcTick"], @@ -12,10 +27,10 @@ for (let [gcTick, label] of [ Bun.gc(true); describe(label, () => { describe("spawnSync", () => { - const hugeString = "hello".repeat(10000).slice(); + const hugeString = "hello".repeat(50000).slice(); it("as an array", () => { - const { stdout } = spawnSync(["echo", "hi"]); + const { stdout } = spawnSync(["node", "-e", "console.log('hi')"]); gcTick(); // stdout is a Buffer const text = stdout!.toString(); @@ -29,7 +44,11 @@ for (let [gcTick, label] of [ stdin: new TextEncoder().encode(hugeString), }); gcTick(); - expect(stdout!.toString()).toBe(hugeString); + const text = stdout!.toString(); + if (text !== hugeString) { + expect(text).toHaveLength(hugeString.length); + expect(text).toBe(hugeString); + } expect(stderr!.byteLength).toBe(0); gcTick(); }); @@ -51,7 +70,7 @@ for (let [gcTick, label] of [ it("throws errors for invalid arguments", async () => { expect(() => { spawnSync({ - cmd: ["echo", "hi"], + cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); }).toThrow("No such file or directory"); @@ -59,15 +78,15 @@ for (let [gcTick, label] of [ }); describe("spawn", () => { - const hugeString = "hello".repeat(10000).slice(); + const hugeString = createHugeString(); it("as an array", async () => { gcTick(); await (async () => { - const { stdout } = spawn(["echo", "hello"], { + const { stdout } = spawn(["node", "-e", "console.log('hello')"], { stdout: "pipe", - stderr: null, - stdin: null, + stderr: "ignore", + stdin: "ignore", }); gcTick(); const text = await new Response(stdout).text(); @@ -79,7 +98,7 @@ for (let [gcTick, label] of [ it("as an array with options object", async () => { gcTick(); const { stdout } = spawn(["printenv", "FOO"], { - cwd: "/tmp", + cwd: tmp, env: { ...process.env, FOO: "bar", @@ -95,16 +114,16 @@ for (let [gcTick, label] of [ }); it("Uint8Array works as stdin", async () => { - rmSync("/tmp/out.123.txt", { force: true }); + rmSync(tmp + "out.123.txt", { force: true }); gcTick(); const { exited } = spawn({ cmd: ["cat"], stdin: new TextEncoder().encode(hugeString), - stdout: Bun.file("/tmp/out.123.txt"), + stdout: Bun.file(tmp + "out.123.txt"), }); gcTick(); await exited; - expect(require("fs").readFileSync("/tmp/out.123.txt", "utf8")).toBe(hugeString); + expect(require("fs").readFileSync(tmp + "out.123.txt", "utf8") == hugeString).toBeTrue(); gcTick(); }); @@ -134,7 +153,9 @@ for (let [gcTick, label] of [ }); it("check exit code from onExit", async () => { - for (let i = 0; i < 1000; i++) { + const count = isWindows ? 100 : 1000; + + for (let i = 0; i < count; i++) { var exitCode1, exitCode2; await new Promise(resolve => { var counter = 0; @@ -177,7 +198,7 @@ for (let [gcTick, label] of [ it.skip("Uint8Array works as stdout", () => { gcTick(); const stdout_buffer = new Uint8Array(11); - const { stdout } = spawnSync(["echo", "hello world"], { + const { stdout } = spawnSync(["node", "-e", "console.log('hello world')"], { stdout: stdout_buffer, stderr: null, stdin: null, @@ -193,7 +214,7 @@ for (let [gcTick, label] of [ it.skip("Uint8Array works as stdout when is smaller than output", () => { gcTick(); const stdout_buffer = new Uint8Array(5); - const { stdout } = spawnSync(["echo", "hello world"], { + const { stdout } = spawnSync(["node", "-e", "console.log('hello world')"], { stdout: stdout_buffer, stderr: null, stdin: null, @@ -209,7 +230,7 @@ for (let [gcTick, label] of [ it.skip("Uint8Array works as stdout when is the exactly size than output", () => { gcTick(); const stdout_buffer = new Uint8Array(12); - const { stdout } = spawnSync(["echo", "hello world"], { + const { stdout } = spawnSync(["node", "-e", "console.log('hello world')"], { stdout: stdout_buffer, stderr: null, stdin: null, @@ -225,7 +246,7 @@ for (let [gcTick, label] of [ it.skip("Uint8Array works as stdout when is larger than output", () => { gcTick(); const stdout_buffer = new Uint8Array(15); - const { stdout } = spawnSync(["echo", "hello world"], { + const { stdout } = spawnSync(["node", "-e", "console.log('hello world')"], { stdout: stdout_buffer, stderr: null, stdin: null, @@ -239,90 +260,111 @@ for (let [gcTick, label] of [ }); it("Blob works as stdin", async () => { - rmSync("/tmp/out.123.txt", { force: true }); + rmSync(tmp + "out.123.txt", { force: true }); gcTick(); const { exited } = spawn({ cmd: ["cat"], stdin: new Blob([new TextEncoder().encode(hugeString)]), - stdout: Bun.file("/tmp/out.123.txt"), + stdout: Bun.file(tmp + "out.123.txt"), }); await exited; - expect(await Bun.file("/tmp/out.123.txt").text()).toBe(hugeString); + expect((await Bun.file(tmp + "out.123.txt").text()) == hugeString).toBeTrue(); }); it("Bun.file() works as stdout", async () => { - rmSync("/tmp/out.123.txt", { force: true }); + rmSync(tmp + "out.123.txt", { force: true }); gcTick(); const { exited } = spawn({ - cmd: ["echo", "hello"], - stdout: Bun.file("/tmp/out.123.txt"), + cmd: ["node", "-e", "console.log('hello')"], + stdout: Bun.file(tmp + "out.123.txt"), }); await exited; gcTick(); - expect(await Bun.file("/tmp/out.123.txt").text()).toBe("hello\n"); + expect(await Bun.file(tmp + "out.123.txt").text()).toBe("hello\n"); }); it("Bun.file() works as stdin", async () => { - await write(Bun.file("/tmp/out.456.txt"), "hello there!"); + await write(Bun.file(tmp + "out.456.txt"), "hello there!"); gcTick(); const { stdout } = spawn({ cmd: ["cat"], stdout: "pipe", - stdin: Bun.file("/tmp/out.456.txt"), + stdin: Bun.file(tmp + "out.456.txt"), }); gcTick(); expect(await readableStreamToText(stdout!)).toBe("hello there!"); }); it("Bun.file() works as stdin and stdout", async () => { - writeFileSync("/tmp/out.456.txt", "hello!"); + writeFileSync(tmp + "out.456.txt", "hello!"); gcTick(); - writeFileSync("/tmp/out.123.txt", "wrong!"); + writeFileSync(tmp + "out.123.txt", "wrong!"); gcTick(); const { exited } = spawn({ cmd: ["cat"], - stdout: Bun.file("/tmp/out.123.txt"), - stdin: Bun.file("/tmp/out.456.txt"), + stdout: Bun.file(tmp + "out.123.txt"), + stdin: Bun.file(tmp + "out.456.txt"), }); gcTick(); await exited; - expect(await Bun.file("/tmp/out.456.txt").text()).toBe("hello!"); + expect(await Bun.file(tmp + "out.456.txt").text()).toBe("hello!"); gcTick(); - expect(await Bun.file("/tmp/out.123.txt").text()).toBe("hello!"); + expect(await Bun.file(tmp + "out.123.txt").text()).toBe("hello!"); }); it("stdout can be read", async () => { - await Bun.write("/tmp/out.txt", hugeString); + await Bun.write(tmp + "out.txt", hugeString); gcTick(); - const { stdout } = spawn({ - cmd: ["cat", "/tmp/out.txt"], - stdout: "pipe", - }); + const promises = new Array(10); + const statusCodes = new Array(10); + for (let i = 0; i < promises.length; i++) { + const { stdout, exited } = spawn({ + cmd: ["cat", tmp + "out.txt"], + stdout: "pipe", + stdin: "ignore", + stderr: "inherit", + }); - gcTick(); + gcTick(); - const text = await readableStreamToText(stdout!); - gcTick(); - expect(text).toBe(hugeString); + promises[i] = readableStreamToText(stdout!); + statusCodes[i] = exited; + gcTick(); + } + + const outputs = await Promise.all(promises); + const statuses = await Promise.all(statusCodes); + + withoutAggressiveGC(() => { + for (let i = 0; i < outputs.length; i++) { + const output = outputs[i]; + const status = statuses[i]; + expect(status).toBe(0); + if (output !== hugeString) { + expect(output.length).toBe(hugeString.length); + } + expect(output).toBe(hugeString); + } + }); }); - it("kill(1) works", async () => { + it("kill(SIGKILL) works", async () => { const process = spawn({ - cmd: ["bash", "-c", "sleep 1000"], + cmd: ["sleep", "1000"], stdout: "pipe", }); gcTick(); const prom = process.exited; - process.kill(1); + process.kill("SIGKILL"); await prom; }); it("kill() works", async () => { const process = spawn({ - cmd: ["bash", "-c", "sleep 1000"], + cmd: ["sleep", "1000"], stdout: "pipe", }); gcTick(); @@ -333,7 +375,7 @@ for (let [gcTick, label] of [ it("stdin can be read and stdout can be written", async () => { const proc = spawn({ - cmd: ["bash", import.meta.dir + "/bash-echo.sh"], + cmd: ["node", "-e", "process.stdin.setRawMode?.(true); process.stdin.pipe(process.stdout)"], stdout: "pipe", stdin: "pipe", lazy: true, @@ -359,8 +401,8 @@ for (let [gcTick, label] of [ done = false; } } - expect(text.trim().length).toBe("hey".length); + expect(text.trim()).toBe("hey"); gcTick(); await proc.exited; @@ -369,9 +411,9 @@ for (let [gcTick, label] of [ describe("pipe", () => { function huge() { return spawn({ - cmd: ["echo", hugeString], + cmd: ["cat"], stdout: "pipe", - stdin: "pipe", + stdin: new Blob([hugeString + "\n"]), stderr: "inherit", lazy: true, }); @@ -379,7 +421,7 @@ for (let [gcTick, label] of [ function helloWorld() { return spawn({ - cmd: ["echo", "hello"], + cmd: ["node", "-e", "console.log('hello')"], stdout: "pipe", stdin: "ignore", }); @@ -407,20 +449,14 @@ for (let [gcTick, label] of [ const process = callback(); var sink = new ArrayBufferSink(); var any = false; - await (async function () { + var { resolve, promise } = Promise.withResolvers(); + + (async function () { var reader = process.stdout?.getReader(); - reader?.closed.then( - a => { - console.log("Closed!"); - }, - err => { - console.log("Closed!", err); - }, - ); var done = false, value; - while (!done) { + while (!done && resolve) { ({ value, done } = await reader!.read()); if (value) { @@ -428,7 +464,11 @@ for (let [gcTick, label] of [ sink.write(value); } } + + resolve && resolve(); + resolve = undefined; })(); + await promise; expect(any).toBe(true); const expected = fixture + "\n"; @@ -452,42 +492,10 @@ for (let [gcTick, label] of [ } }); - describe("ipc", () => { - it("the subprocess should be defined and the child should send", done => { - gcTick(); - const returned_subprocess = spawn([bunExe(), path.join(__dirname, "bun-ipc-child.js")], { - ipc: (message, subProcess) => { - expect(subProcess).toBe(returned_subprocess); - expect(message).toBe("hello"); - subProcess.kill(); - done(); - gcTick(); - }, - }); - }); - - it("the subprocess should receive the parent message and respond back", done => { - gcTick(); - - const parentMessage = "I am your father"; - const childProc = spawn([bunExe(), path.join(__dirname, "bun-ipc-child-respond.js")], { - ipc: (message, subProcess) => { - expect(message).toBe(`pong:${parentMessage}`); - subProcess.kill(); - done(); - gcTick(); - }, - }); - - childProc.send(parentMessage); - gcTick(); - }); - }); - it("throws errors for invalid arguments", async () => { expect(() => { spawnSync({ - cmd: ["echo", "hi"], + cmd: ["node", "-e", "console.log('hi')"], cwd: "./this-should-not-exist", }); }).toThrow("No such file or directory"); @@ -496,7 +504,8 @@ for (let [gcTick, label] of [ }); } -if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD) { +// This is a posix only test +if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD && !isWindows) { it("with BUN_FEATURE_FLAG_FORCE_WAITER_THREAD", async () => { const result = spawnSync({ cmd: [bunExe(), "test", path.resolve(import.meta.path)], @@ -516,7 +525,8 @@ if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD) { describe("spawn unref and kill should not hang", () => { it("kill and await exited", async () => { - for (let i = 0; i < 10; i++) { + const promises = new Array(10); + for (let i = 0; i < promises.length; i++) { const proc = spawn({ cmd: ["sleep", "0.001"], stdout: "ignore", @@ -524,49 +534,56 @@ describe("spawn unref and kill should not hang", () => { stdin: "ignore", }); proc.kill(); - await proc.exited; + promises[i] = proc.exited; } + await Promise.all(promises); + expect().pass(); }); it("unref", async () => { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 10; i++) { const proc = spawn({ cmd: ["sleep", "0.001"], stdout: "ignore", stderr: "ignore", stdin: "ignore", }); - proc.unref(); + // TODO: on Windows + if (!isWindows) proc.unref(); await proc.exited; } expect().pass(); }); it("kill and unref", async () => { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < (isWindows ? 10 : 100); i++) { const proc = spawn({ cmd: ["sleep", "0.001"], stdout: "ignore", stderr: "ignore", stdin: "ignore", }); + proc.kill(); - proc.unref(); + if (!isWindows) proc.unref(); + await proc.exited; + console.count("Finished"); } expect().pass(); }); it("unref and kill", async () => { - for (let i = 0; i < 100; i++) { + for (let i = 0; i < (isWindows ? 10 : 100); i++) { const proc = spawn({ cmd: ["sleep", "0.001"], stdout: "ignore", stderr: "ignore", stdin: "ignore", }); - proc.unref(); + // TODO: on Windows + if (!isWindows) proc.unref(); proc.kill(); await proc.exited; } @@ -574,6 +591,7 @@ describe("spawn unref and kill should not hang", () => { expect().pass(); }); + // process.unref() on Windows does not work ye :( it("should not hang after unref", async () => { const proc = spawn({ cmd: [bunExe(), path.join(import.meta.dir, "does-not-hang.js")], @@ -585,8 +603,8 @@ describe("spawn unref and kill should not hang", () => { }); async function runTest(sleep: string, order = ["sleep", "kill", "unref", "exited"]) { - console.log("running", order.join(",")); - for (let i = 0; i < 100; i++) { + console.log("running", order.join(","), "x 100"); + for (let i = 0; i < (isWindows ? 10 : 100); i++) { const proc = spawn({ cmd: ["sleep", sleep], stdout: "ignore", @@ -625,31 +643,41 @@ async function runTest(sleep: string, order = ["sleep", "kill", "unref", "exited } describe("should not hang", () => { - for (let sleep of ["0.001", "0"]) { - describe("sleep " + sleep, () => { - for (let order of [ - ["sleep", "kill", "unref", "exited"], - ["sleep", "unref", "kill", "exited"], - ["kill", "sleep", "unref", "exited"], - ["kill", "unref", "sleep", "exited"], - ["unref", "sleep", "kill", "exited"], - ["unref", "kill", "sleep", "exited"], - ["exited", "sleep", "kill", "unref"], - ["exited", "sleep", "unref", "kill"], - ["exited", "kill", "sleep", "unref"], - ["exited", "kill", "unref", "sleep"], - ["exited", "unref", "sleep", "kill"], - ["exited", "unref", "kill", "sleep"], - ["unref", "exited"], - ["exited", "unref"], - ["kill", "exited"], - ["exited"], - ]) { - const name = order.join(","); - const fn = runTest.bind(undefined, sleep, order); - it(name, fn); - } - }); + for (let sleep of ["0", "0.1"]) { + it( + "sleep " + sleep, + () => { + const runs = []; + for (let order of [ + ["sleep", "kill", "unref", "exited"], + ["sleep", "unref", "kill", "exited"], + ["kill", "sleep", "unref", "exited"], + ["kill", "unref", "sleep", "exited"], + ["unref", "sleep", "kill", "exited"], + ["unref", "kill", "sleep", "exited"], + ["exited", "sleep", "kill", "unref"], + ["exited", "sleep", "unref", "kill"], + ["exited", "kill", "sleep", "unref"], + ["exited", "kill", "unref", "sleep"], + ["exited", "unref", "sleep", "kill"], + ["exited", "unref", "kill", "sleep"], + ["unref", "exited"], + ["exited", "unref"], + ["kill", "exited"], + ["exited"], + ]) { + runs.push( + runTest(sleep, order).catch(err => { + console.error("For order", JSON.stringify(order, null, 2)); + throw err; + }), + ); + } + + return Promise.all(runs); + }, + 128_000, + ); } }); @@ -658,7 +686,7 @@ it("#3480", async () => { var server = Bun.serve({ port: 0, fetch: (req, res) => { - Bun.spawnSync(["echo", "1"], {}); + Bun.spawnSync(["node", "-e", "console.log('1')"], {}); return new Response("Hello world!"); }, }); @@ -670,3 +698,73 @@ it("#3480", async () => { server!.stop(true); } }); + +describe("close handling", () => { + var testNumber = 0; + for (let stdin_ of [() => openSync(import.meta.path, "r"), "ignore", Bun.stdin, undefined as any] as const) { + const stdinFn = typeof stdin_ === "function" ? stdin_ : () => stdin_; + for (let stdout of [1, "ignore", Bun.stdout, undefined as any] as const) { + for (let stderr of [2, "ignore", Bun.stderr, undefined as any] as const) { + const thisTest = testNumber++; + it(`#${thisTest} [ ${typeof stdin_ === "function" ? "fd" : stdin_}, ${stdout}, ${stderr} ]`, async () => { + const stdin = stdinFn(); + + function getExitPromise() { + const { exited: proc1Exited } = spawn({ + cmd: ["node", "-e", "console.log('" + "Executing test " + thisTest + "')"], + stdin, + stdout, + stderr, + }); + + const { exited: proc2Exited } = spawn({ + cmd: ["node", "-e", "console.log('" + "Executing test " + thisTest + "')"], + stdin, + stdout, + stderr, + }); + + return Promise.all([proc1Exited, proc2Exited]); + } + + // We do this to try to force the GC to finalize the Subprocess objects. + await (async function () { + let exitPromise = getExitPromise(); + + if (typeof stdin === "number") { + expect(() => fstatSync(stdin)).not.toThrow(); + } + + if (typeof stdout === "number") { + expect(() => fstatSync(stdout)).not.toThrow(); + } + + if (typeof stderr === "number") { + expect(() => fstatSync(stderr)).not.toThrow(); + } + + await exitPromise; + })(); + Bun.gc(false); + await Bun.sleep(0); + + if (typeof stdin === "number") { + expect(() => fstatSync(stdin)).not.toThrow(); + } + + if (typeof stdout === "number") { + expect(() => fstatSync(stdout)).not.toThrow(); + } + + if (typeof stderr === "number") { + expect(() => fstatSync(stderr)).not.toThrow(); + } + + if (typeof stdin === "number") { + closeSync(stdin); + } + }); + } + } + } +}); diff --git a/test/js/bun/spawn/stdin-repro.js b/test/js/bun/spawn/stdin-repro.js index 51b101764afe83..40d2569c4305b0 100644 --- a/test/js/bun/spawn/stdin-repro.js +++ b/test/js/bun/spawn/stdin-repro.js @@ -1,12 +1,13 @@ var stdout = Bun.stdout.writer(); -console.error("Started"); var count = 0; -// const file = Bun.file("/tmp/testpipe"); const file = Bun.stdin; + for await (let chunk of file.stream()) { - const str = new Buffer(chunk).toString(); - stdout.write(str); + stdout.write(chunk); await stdout.flush(); count++; } -console.error("Finished with", count); + +if (count < 2) { + throw new Error("Expected to receive at least 2 chunks, got " + count); +} diff --git a/test/js/bun/test/test-test.test.ts b/test/js/bun/test/test-test.test.ts index d1dccd68007c15..aabc3882c6d83b 100644 --- a/test/js/bun/test/test-test.test.ts +++ b/test/js/bun/test/test-test.test.ts @@ -34,7 +34,7 @@ it("shouldn't crash when async test runner callback throws", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test", "bad.test.js"], cwd: test_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: bunEnv, @@ -296,7 +296,7 @@ it("should return non-zero exit code for invalid syntax", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test", "bad.test.js"], cwd: test_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: bunEnv, @@ -325,7 +325,7 @@ it("invalid syntax counts towards bail", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test", "--bail=3"], cwd: test_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: bunEnv, @@ -638,7 +638,7 @@ describe("empty", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test", "empty.test.js"], cwd: test_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env: bunEnv, diff --git a/test/js/bun/util/filesink.test.ts b/test/js/bun/util/filesink.test.ts index 5d062e266d3708..b76a98cac0476c 100644 --- a/test/js/bun/util/filesink.test.ts +++ b/test/js/bun/util/filesink.test.ts @@ -2,32 +2,23 @@ import { ArrayBufferSink } from "bun"; import { describe, expect, it } from "bun:test"; import { mkfifo } from "mkfifo"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; describe("FileSink", () => { - const fixtures = [ - [ - ["abcdefghijklmnopqrstuvwxyz"], - new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz"), - "abcdefghijklmnopqrstuvwxyz", - ], + const fixturesInput = [ + [["abcdefghijklmnopqrstuvwxyz"], "abcdefghijklmnopqrstuvwxyz"], [ ["abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"], - new TextEncoder().encode("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", ], - [ - ["😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"], - new TextEncoder().encode("😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"), - "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", - ], + [["😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"], "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"], [ ["abcdefghijklmnopqrstuvwxyz", "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"], - new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"), "abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", ], [ ["abcdefghijklmnopqrstuvwxyz", "😋", " Get Emoji — All Emojis", " to ✂️ Copy and 📋 Paste 👌"], - new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"), "(rope) " + "abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", ], [ @@ -37,13 +28,24 @@ describe("FileSink", () => { " Get Emoji — All Emojis", " to ✂️ Copy and 📋 Paste 👌", ], - new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"), "(array) " + "abcdefghijklmnopqrstuvwxyz" + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", ], ] as const; + const fixtures = fixturesInput.map(([input, label]) => { + let expected; + + if (Array.isArray(input)) { + expected = Buffer.concat(input.map(str => Buffer.from(str))); + } else { + expected = Buffer.from(input as any); + } + + return [input, expected, label] as const; + }); + function getPath(label: string) { - const path = `/tmp/bun-test-${Bun.hash(label).toString(10)}.txt`; + const path = join(tmpdir(), `bun-test-${Bun.hash(label).toString(10)}.${(Math.random() * 1_000_000) | 0}.txt`); try { require("fs").unlinkSync(path); } catch (e) {} @@ -53,27 +55,31 @@ describe("FileSink", () => { var activeFIFO: Promise; var decoder = new TextDecoder(); - function getFd(label: string) { - const path = `/tmp/bun-test-${Bun.hash(label).toString(10)}.txt`; + function getFd(label: string, byteLength = 0) { + const path = join(tmpdir(), `bun-test-${Bun.hash(label).toString(10)}.${(Math.random() * 1_000_000) | 0}.txt`); try { require("fs").unlinkSync(path); } catch (e) {} mkfifo(path, 0o666); - activeFIFO = (async function (stream: ReadableStream) { + activeFIFO = (async function (stream: ReadableStream, byteLength = 0) { var chunks: Uint8Array[] = []; + const original = byteLength; + var got = 0; for await (const chunk of stream) { chunks.push(chunk); + got += chunk.byteLength; } + if (got !== original) throw new Error(`Expected ${original} bytes, got ${got} (${label})`); return Buffer.concat(chunks).toString(); // test it on a small chunk size - })(Bun.file(path).stream(64)); + })(Bun.file(path).stream(64), byteLength); return path; } for (let isPipe of [true, false] as const) { describe(isPipe ? "pipe" : "file", () => { - for (const [input, expected, label] of fixtures) { - var getPathOrFd = () => (isPipe ? getFd(label) : getPath(label)); + fixtures.forEach(([input, expected, label]) => { + const getPathOrFd = () => (isPipe ? getFd(label, expected.byteLength) : getPath(label)); it(`${JSON.stringify(label)}`, async () => { const path = getPathOrFd(); @@ -136,7 +142,7 @@ describe("FileSink", () => { expect(output).toBe(decoder.decode(expected)); } }); - } + }); }); } }); diff --git a/test/js/node/child_process/child-process-stdio.test.js b/test/js/node/child_process/child-process-stdio.test.js index 3be90dc49a17ed..2585841b68d77c 100644 --- a/test/js/node/child_process/child-process-stdio.test.js +++ b/test/js/node/child_process/child-process-stdio.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect, beforeAll } from "bun:test"; import { spawn, execSync } from "node:child_process"; import { bunExe, bunEnv } from "harness"; @@ -10,6 +9,7 @@ describe("process.stdout", () => { it("should allow us to write to it", done => { const child = spawn(bunExe(), [CHILD_PROCESS_FILE, "STDOUT"], { env: bunEnv, + stdio: ["inherit", "pipe", "inherit"], }); child.stdout.setEncoding("utf8"); child.stdout.on("data", data => { @@ -29,6 +29,7 @@ describe("process.stdin", () => { // Child should read from stdin and write it back const child = spawn(bunExe(), [CHILD_PROCESS_FILE, "STDIN", "READABLE"], { env: bunEnv, + stdio: ["pipe", "pipe", "inherit"], }); let data = ""; child.stdout.setEncoding("utf8"); @@ -44,8 +45,9 @@ describe("process.stdin", () => { done(err); } }); - child.stdin.write(input); - child.stdin.end(); + child.stdin.write(input, function () { + child.stdin.end(...arguments); + }); }); it("should allow us to read from stdin via flowing mode", done => { @@ -53,6 +55,7 @@ describe("process.stdin", () => { // Child should read from stdin and write it back const child = spawn(bunExe(), [CHILD_PROCESS_FILE, "STDIN", "FLOWING"], { env: bunEnv, + stdio: ["pipe", "pipe", "inherit"], }); let data = ""; child.stdout.setEncoding("utf8"); @@ -71,16 +74,18 @@ describe("process.stdin", () => { done(err); } }); - child.stdin.write(input); - child.stdin.end(); + child.stdin.end(input); }); it("should allow us to read > 65kb from stdin", done => { - const numReps = Math.ceil((66 * 1024) / 5); - const input = "hello".repeat(numReps); + const numReps = Math.ceil((1024 * 1024) / 5); + const input = Buffer.alloc("hello".length * numReps) + .fill("hello") + .toString(); // Child should read from stdin and write it back const child = spawn(bunExe(), [CHILD_PROCESS_FILE, "STDIN", "FLOWING"], { - env: bunEnv, + env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "1" }, + stdio: ["pipe", "pipe", "inherit"], }); let data = ""; child.stdout.setEncoding("utf8"); @@ -93,14 +98,15 @@ describe("process.stdin", () => { }) .on("end", function () { try { - expect(data).toBe(`data: ${input}`); + const expected = "data: " + input; + expect(data.length).toBe(expected.length); + expect(data).toBe(expected); done(); } catch (err) { done(err); } }); - child.stdin.write(input); - child.stdin.end(); + child.stdin.end(input); }); it("should allow us to read from a file", () => { @@ -111,42 +117,3 @@ describe("process.stdin", () => { expect(result).toEqual("data: File read successfully"); }); }); - -describe("process.stdio pipes", () => { - it("is writable", () => { - const child = spawn(bunExe(), [import.meta.dir + "/fixtures/child-process-pipe-read.js"], { - env: bunEnv, - stdio: ["pipe", "pipe", "pipe", "pipe"], - }); - const pipe = child.stdio[3]; - expect(pipe).not.toBe(null); - pipe.write("stdout_test"); - - child.stdout.on("data", data => { - try { - expect(data).toBe("stdout_test"); - done(); - } catch (err) { - done(err); - } - }); - }); - - it("is readable", () => { - const child = spawn(bunExe(), [import.meta.dir + "/fixtures/child-process-pipe-read.js"], { - env: bunEnv, - stdio: ["pipe", "pipe", "pipe", "pipe"], - }); - const pipe = child.stdio[3]; - expect(pipe).not.toBe(null); - - child.stdout.on("data", data => { - try { - expect(data).toBe("stdout_test"); - done(); - } catch (err) { - done(err); - } - }); - }); -}); diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index 128b8b5957e167..57bfb47af073f7 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -4,10 +4,16 @@ import { createTest } from "node-harness"; import { tmpdir } from "node:os"; import path from "node:path"; import util from "node:util"; -import { bunEnv, bunExe } from "harness"; -const { beforeAll, describe, expect, it, throws, assert, createCallCheckCtx, createDoneDotAll } = createTest( - import.meta.path, -); +import { bunEnv, bunExe, isWindows } from "harness"; +const { beforeAll, beforeEach, afterAll, describe, expect, it, throws, assert, createCallCheckCtx, createDoneDotAll } = + createTest(import.meta.path); +const origProcessEnv = process.env; +beforeEach(() => { + process.env = { ...bunEnv }; +}); +afterAll(() => { + process.env = origProcessEnv; +}); const strictEqual = (a, b) => expect(a).toStrictEqual(b); const debug = process.env.DEBUG ? console.log : () => {}; @@ -46,7 +52,7 @@ const fixtures = { // USE OR OTHER DEALINGS IN THE SOFTWARE. const common = { - pwdCommand: ["pwd", []], + pwdCommand: isWindows ? ["node", ["-e", "process.stdout.write(process.cwd() + '\\n')"]] : ["pwd", []], }; describe("ChildProcess.constructor", () => { @@ -243,7 +249,7 @@ describe("child_process cwd", () => { const { mustCall } = createCallCheckCtx(createDone(1500)); const exitDone = createDone(5000); - const child = spawn(...common.pwdCommand, options); + const child = spawn(...common.pwdCommand, { stdio: ["inherit", "pipe", "inherit"], ...options }); strictEqual(typeof child.pid, expectPidType); @@ -267,7 +273,7 @@ describe("child_process cwd", () => { } }); - child.on( + child.stdout.on( "close", mustCall(() => { expectData && strictEqual(data.trim(), expectData); @@ -286,7 +292,7 @@ describe("child_process cwd", () => { // mustCall(function (e) { // console.log(e); // strictEqual(e.code, "ENOENT"); - // }) + // }), // ); // }); @@ -331,7 +337,7 @@ describe("child_process cwd", () => { }, createDone(1500), ); - const shouldExistDir = "/dev"; + const shouldExistDir = isWindows ? "C:\\Windows\\System32" : "/dev"; testCwd( { cwd: shouldExistDir }, { @@ -363,10 +369,8 @@ describe("child_process cwd", () => { describe("child_process default options", () => { it("should use process.env as default env", done => { - const origTmpDir = globalThis.process.env.TMPDIR; - globalThis.process.env.TMPDIR = platformTmpDir; + process.env.TMPDIR = platformTmpDir; let child = spawn("printenv", [], {}); - globalThis.process.env.TMPDIR = origTmpDir; let response = ""; child.stdout.setEncoding("utf8"); @@ -389,7 +393,7 @@ describe("child_process default options", () => { }); describe("child_process double pipe", () => { - it.skipIf(process.platform === "linux")("should allow two pipes to be used at once", done => { + it("should allow two pipes to be used at once", done => { // const { mustCallAtLeast, mustCall } = createCallCheckCtx(done); const mustCallAtLeast = fn => fn; const mustCall = fn => fn; @@ -503,7 +507,7 @@ describe("fork", () => { const { mustCall } = createCallCheckCtx(done); const ac = new AbortController(); const { signal } = ac; - const cp = fork(fixtures.path("child-process-stay-alive-forever.js", { env: bunEnv }), { + const cp = fork(fixtures.path("child-process-stay-alive-forever.js"), { signal, env: bunEnv, }); @@ -643,34 +647,40 @@ describe("fork", () => { }); }); }); + // This test fails due to a DataCloneError or due to "Unable to deserialize data." + // This test was originally marked as TODO before the process changes. it.todo( "Ensure that the second argument of `fork` and `fork` should parse options correctly if args is undefined or null", done => { const invalidSecondArgs = [0, true, () => {}, Symbol("t")]; - invalidSecondArgs.forEach(arg => { - expect(() => fork(fixtures.path("child-process-echo-options.js"), arg)).toThrow({ - code: "ERR_INVALID_ARG_TYPE", - name: "TypeError", - message: `The \"args\" argument must be of type Array. Received ${arg?.toString()}`, + try { + invalidSecondArgs.forEach(arg => { + expect(() => fork(fixtures.path("child-process-echo-options.js"), arg)).toThrow({ + code: "ERR_INVALID_ARG_TYPE", + name: "TypeError", + message: `The \"args\" argument must be of type Array. Received ${arg?.toString()}`, + }); }); - }); + } catch (e) { + done(e); + return; + } - const argsLists = [undefined, null, []]; + const argsLists = [[]]; const { mustCall } = createCallCheckCtx(done); argsLists.forEach(args => { const cp = fork(fixtures.path("child-process-echo-options.js"), args, { - env: { ...process.env, ...expectedEnv, ...bunEnv }, + env: { ...bunEnv, ...expectedEnv }, }); - // TODO - bun has no `send` method in the process - // cp.on( - // 'message', - // common.mustCall(({ env }) => { - // assert.strictEqual(env.foo, expectedEnv.foo); - // }) - // ); + cp.on( + "message", + mustCall(({ env }) => { + assert.strictEqual(env.foo, expectedEnv.foo); + }), + ); cp.on( "exit", @@ -722,7 +732,7 @@ describe("fork", () => { // https://github.com/nodejs/node/blob/v20.5.0/test/parallel/test-child-process-fork-stdio.js }); describe("fork", () => { - it.todo("message", () => { + it.todo("message", done => { // TODO - bun has no `send` method in the process const { mustCall } = createCallCheckCtx(done); const args = ["foo", "bar"]; diff --git a/test/js/node/child_process/child_process.test.ts b/test/js/node/child_process/child_process.test.ts index 367b11a6ff4e0c..e7a604cb57066c 100644 --- a/test/js/node/child_process/child_process.test.ts +++ b/test/js/node/child_process/child_process.test.ts @@ -1,19 +1,40 @@ -// @known-failing-on-windows: 1 failing -import { describe, it, expect } from "bun:test"; +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "bun:test"; import { ChildProcess, spawn, execFile, exec, fork, spawnSync, execFileSync, execSync } from "node:child_process"; import { tmpdir } from "node:os"; import { promisify } from "node:util"; -import { bunExe, bunEnv } from "harness"; +import { bunExe, bunEnv, isWindows } from "harness"; import path from "path"; - +import { semver } from "bun"; +import fs from "fs"; const debug = process.env.DEBUG ? console.log : () => {}; +const originalProcessEnv = process.env; +beforeEach(() => { + process.env = { ...bunEnv }; + // Github actions might filter these out + for (const key in process.env) { + if (key.toUpperCase().startsWith("TLS_")) { + delete process.env[key]; + } + } +}); + +afterAll(() => { + process.env = originalProcessEnv; +}); + const platformTmpDir = require("fs").realpathSync(tmpdir()); -// Semver regex: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39?permalink_comment_id=2896416#gistcomment-2896416 -// Not 100% accurate, but good enough for this test -const SEMVER_REGEX = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[a-zA-Z\d][-a-zA-Z.\d]*)?(\+[a-zA-Z\d][-a-zA-Z.\d]*)?$/; +function isValidSemver(str) { + const cmp = str.replaceAll("-debug", "").trim(); + const valid = semver.satisfies(cmp, "*"); + + if (!valid) { + console.error(`Invalid semver: ${JSON.stringify(cmp)}`); + } + + return valid; +} describe("ChildProcess.spawn()", () => { it("should emit `spawn` on spawn", async () => { @@ -75,7 +96,7 @@ describe("spawn()", () => { debug(`stderr: ${data}`); }); }); - expect(SEMVER_REGEX.test(result.trim())).toBe(true); + expect(isValidSemver(result.trim().replace("-debug", ""))).toBe(true); }); it("should allow stdout to be read via .read() API", async () => { @@ -94,7 +115,7 @@ describe("spawn()", () => { resolve(finalData); }); }); - expect(SEMVER_REGEX.test(result.trim())).toBe(true); + expect(isValidSemver(result.trim())).toBe(true); }); it("should accept stdio option with 'ignore' for no stdio fds", async () => { @@ -117,7 +138,7 @@ describe("spawn()", () => { }); it("should allow us to set cwd", async () => { - const child = spawn("pwd", { cwd: platformTmpDir }); + const child = spawn(bunExe(), ["-e", "console.log(process.cwd())"], { cwd: platformTmpDir, env: bunEnv }); const result: string = await new Promise(resolve => { child.stdout.on("data", data => { resolve(data.toString()); @@ -152,24 +173,40 @@ describe("spawn()", () => { }); it("should allow us to set env", async () => { - async function getChildEnv(env: any): Promise { - const child = spawn("env", { env: env }); - const result: string = await new Promise(resolve => { + async function getChildEnv(env: any): Promise { + const child = spawn("printenv", { + env: env, + stdio: ["inherit", "pipe", "inherit"], + }); + const result: object = await new Promise(resolve => { let output = ""; child.stdout.on("data", data => { output += data; }); child.stdout.on("end", () => { - resolve(output); + const envs = output + .split("\n") + .map(env => env.trim().split("=")) + .filter(env => env.length === 2 && env[0]); + const obj = Object.fromEntries(envs); + resolve(obj); }); }); return result; } - expect(/TEST\=test/.test(await getChildEnv({ TEST: "test" }))).toBe(true); - expect(await getChildEnv({})).toStrictEqual(""); - expect(await getChildEnv(undefined)).not.toStrictEqual(""); - expect(await getChildEnv(null)).not.toStrictEqual(""); + // on Windows, there's a set of environment variables which are always set + if (isWindows) { + expect(await getChildEnv({ TEST: "test" })).toMatchObject({ TEST: "test" }); + expect(await getChildEnv({})).toMatchObject({}); + expect(await getChildEnv(undefined)).not.toStrictEqual({}); + expect(await getChildEnv(null)).not.toStrictEqual({}); + } else { + expect(await getChildEnv({ TEST: "test" })).toEqual({ TEST: "test" }); + expect(await getChildEnv({})).toEqual({}); + expect(await getChildEnv(undefined)).toMatchObject(process.env); + expect(await getChildEnv(null)).toMatchObject(process.env); + } }); it("should allow explicit setting of argv0", async () => { @@ -178,7 +215,14 @@ describe("spawn()", () => { resolve = resolve1; }); process.env.NO_COLOR = "1"; - const child = spawn("node", ["--help"], { argv0: bunExe() }); + const child = spawn( + "node", + ["-e", "console.log(JSON.stringify([process.argv0, fs.realpathSync(process.argv[0])]))"], + { + argv0: bunExe(), + stdio: ["inherit", "pipe", "inherit"], + }, + ); delete process.env.NO_COLOR; let msg = ""; @@ -191,7 +235,7 @@ describe("spawn()", () => { }); const result = await promise; - expect(/bun.sh\/docs/.test(result)).toBe(true); + expect(JSON.parse(result)).toStrictEqual([bunExe(), fs.realpathSync(Bun.which("node"))]); }); it("should allow us to spawn in a shell", async () => { @@ -207,8 +251,12 @@ describe("spawn()", () => { resolve(data.toString()); }); }); - expect(result1.trim()).toBe(Bun.which("sh")); - expect(result2.trim()).toBe(Bun.which("bash")); + + // on Windows it will run in comamnd prompt + // we know it's command prompt because it's the only shell that doesn't support $0. + expect(result1.trim()).toBe(isWindows ? "$0" : "/bin/sh"); + + expect(result2.trim()).toBe("bash"); }); it("should spawn a process synchronously", () => { const { stdout } = spawnSync("echo", ["hello"], { encoding: "utf8" }); @@ -226,7 +274,7 @@ describe("execFile()", () => { resolve(stdout); }); }); - expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true); + expect(isValidSemver(result.toString().trim())).toBe(true); }); }); @@ -240,7 +288,7 @@ describe("exec()", () => { resolve(stdout); }); }); - expect(SEMVER_REGEX.test(result.toString().trim())).toBe(true); + expect(isValidSemver(result.toString().trim())).toBe(true); }); it("should return an object w/ stdout and stderr when promisified", async () => { @@ -250,7 +298,7 @@ describe("exec()", () => { expect(typeof result.stderr).toBe("string"); const { stdout, stderr } = result; - expect(SEMVER_REGEX.test(stdout.trim())).toBe(true); + expect(isValidSemver(stdout.trim())).toBe(true); expect(stderr.trim()).toBe(""); }); }); @@ -264,14 +312,15 @@ describe("spawnSync()", () => { describe("execFileSync()", () => { it("should execute a file synchronously", () => { - const result = execFileSync(bunExe(), ["-v"], { encoding: "utf8" }); - expect(SEMVER_REGEX.test(result.trim())).toBe(true); + const result = execFileSync(bunExe(), ["-v"], { encoding: "utf8", env: process.env }); + expect(isValidSemver(result.trim())).toBe(true); }); it("should allow us to pass input to the command", () => { const result = execFileSync("node", [import.meta.dir + "/spawned-child.js", "STDIN"], { input: "hello world!", encoding: "utf8", + env: process.env, }); expect(result.trim()).toBe("data: hello world!"); }); @@ -279,8 +328,8 @@ describe("execFileSync()", () => { describe("execSync()", () => { it("should execute a command in the shell synchronously", () => { - const result = execSync("bun -v", { encoding: "utf8" }); - expect(SEMVER_REGEX.test(result.trim())).toBe(true); + const result = execSync(bunExe() + " -v", { encoding: "utf8", env: bunEnv }); + expect(isValidSemver(result.trim())).toBe(true); }); }); @@ -289,6 +338,7 @@ describe("Bun.spawn()", () => { const proc = Bun.spawn({ cmd: ["echo", "hello"], stdout: "pipe", + env: bunEnv, }); for await (const chunk of proc.stdout) { @@ -318,6 +368,8 @@ it("should call close and exit before process exits", async () => { cwd: import.meta.dir, env: bunEnv, stdout: "pipe", + stdin: "inherit", + stderr: "inherit", }); await proc.exited; expect(proc.exitCode).toBe(0); diff --git a/test/js/node/child_process/fixtures/child-process-echo-options.js b/test/js/node/child_process/fixtures/child-process-echo-options.js index 7d6298bd02905b..0f5be894af8c39 100644 --- a/test/js/node/child_process/fixtures/child-process-echo-options.js +++ b/test/js/node/child_process/fixtures/child-process-echo-options.js @@ -1,2 +1,3 @@ // TODO - bun has no `send` method in the process -process?.send({ env: process.env }); +const out = { env: { ...process.env } }; +process?.send(out); diff --git a/test/js/node/child_process/spawned-child.js b/test/js/node/child_process/spawned-child.js index 263c566f98d53b..f2365fba7d80b2 100644 --- a/test/js/node/child_process/spawned-child.js +++ b/test/js/node/child_process/spawned-child.js @@ -7,7 +7,7 @@ if (TARGET === "STDIN") { if (MODE === "READABLE") { process.stdin.on("readable", () => { let chunk; - while ((chunk = process.stdin.read()) !== null) { + while ((chunk = process.stdin.read()) != null) { data += chunk; } }); @@ -17,8 +17,7 @@ if (TARGET === "STDIN") { }); } process.stdin.on("end", () => { - process.stdout.write("data: "); - process.stdout.write(data); + process.stdout.write(Buffer.concat([Buffer.from("data: "), Buffer.from(data)])); }); } else if (TARGET === "STDOUT") { process.stdout.write("stdout_test"); diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index 0598c25ba6c318..324fe24c961de4 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -25,13 +25,21 @@ import { test, it, expect, describe } from "bun:test"; import { createContext, Script } from "node:vm"; import fs from "fs"; import path from "path"; +import { isWindows } from "harness"; -const publicPem = fs.readFileSync(path.join(import.meta.dir, "fixtures", "rsa_public.pem"), "ascii"); -const privatePem = fs.readFileSync(path.join(import.meta.dir, "fixtures", "rsa_private.pem"), "ascii"); -const privateEncryptedPem = fs.readFileSync( - path.join(import.meta.dir, "fixtures", "rsa_private_encrypted.pem"), - "ascii", -); +function readFile(...args) { + const result = fs.readFileSync(...args); + + if (isWindows) { + return result.replace(/\r\n/g, "\n"); + } + + return result; +} + +const publicPem = readFile(path.join(import.meta.dir, "fixtures", "rsa_public.pem"), "ascii"); +const privatePem = readFile(path.join(import.meta.dir, "fixtures", "rsa_private.pem"), "ascii"); +const privateEncryptedPem = readFile(path.join(import.meta.dir, "fixtures", "rsa_private_encrypted.pem"), "ascii"); // Constructs a regular expression for a PEM-encoded key with the given label. function getRegExpForPEM(label: string, cipher?: string) { @@ -337,8 +345,8 @@ describe("crypto.KeyObjects", () => { [ { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ed25519_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ed25519_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ed25519_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ed25519_public.pem"), "ascii"), keyType: "ed25519", jwk: { crv: "Ed25519", @@ -348,8 +356,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ed448_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ed448_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ed448_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ed448_public.pem"), "ascii"), keyType: "ed448", jwk: { crv: "Ed448", @@ -359,8 +367,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "x25519_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "x25519_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "x25519_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "x25519_public.pem"), "ascii"), keyType: "x25519", jwk: { crv: "X25519", @@ -370,8 +378,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "x448_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "x448_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "x448_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "x448_public.pem"), "ascii"), keyType: "x448", jwk: { crv: "X448", @@ -431,8 +439,8 @@ describe("crypto.KeyObjects", () => { [ { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p256_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p256_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ec_p256_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ec_p256_public.pem"), "ascii"), keyType: "ec", namedCurve: "prime256v1", jwk: { @@ -444,8 +452,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_secp256k1_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_secp256k1_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ec_secp256k1_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ec_secp256k1_public.pem"), "ascii"), keyType: "ec", namedCurve: "secp256k1", jwk: { @@ -457,8 +465,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p384_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p384_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ec_p384_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ec_p384_public.pem"), "ascii"), keyType: "ec", namedCurve: "secp384r1", jwk: { @@ -470,8 +478,8 @@ describe("crypto.KeyObjects", () => { }, }, { - private: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p521_private.pem"), "ascii"), - public: fs.readFileSync(path.join(import.meta.dir, "fixtures", "ec_p521_public.pem"), "ascii"), + private: readFile(path.join(import.meta.dir, "fixtures", "ec_p521_private.pem"), "ascii"), + public: readFile(path.join(import.meta.dir, "fixtures", "ec_p521_public.pem"), "ascii"), keyType: "ec", namedCurve: "secp521r1", jwk: { @@ -581,11 +589,8 @@ describe("crypto.KeyObjects", () => { [2048, 4096].forEach(suffix => { test(`RSA-${suffix} should work`, async () => { { - const publicPem = fs.readFileSync(path.join(import.meta.dir, "fixtures", `rsa_public_${suffix}.pem`), "ascii"); - const privatePem = fs.readFileSync( - path.join(import.meta.dir, "fixtures", `rsa_private_${suffix}.pem`), - "ascii", - ); + const publicPem = readFile(path.join(import.meta.dir, "fixtures", `rsa_public_${suffix}.pem`), "ascii"); + const privatePem = readFile(path.join(import.meta.dir, "fixtures", `rsa_private_${suffix}.pem`), "ascii"); const publicKey = createPublicKey(publicPem); const expectedKeyDetails = { modulusLength: suffix, diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index 9ff1061b7743ba..fa1dc510a8c643 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -590,7 +590,7 @@ describe("EventEmitter.on", () => { const path = require("node:path"); const fpath = path.join(__filename, "..", "..", "child_process", "fixtures", "child-process-echo-options.js"); - console.log(fpath); + const text = await Bun.file(fpath).text(); const interfaced = createInterface(createReadStream(fpath)); const output = []; @@ -598,12 +598,9 @@ describe("EventEmitter.on", () => { for await (const line of interfaced) { output.push(line); } - } catch (e) { - expect(output).toBe([ - "// TODO - bun has no `send` method in the process", - "process?.send({ env: process.env });", - ]); - } + } catch (e) {} + const out = text.replaceAll("\r\n", "\n").trim().split("\n"); + expect(output).toEqual(out); }); }); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 02f0e1b7b9d960..755348522327f2 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1481,7 +1481,7 @@ describe("rmdirSync", () => { }); }); -describe.skipIf(isWindows)("createReadStream", () => { +describe("createReadStream", () => { it("works (1 chunk)", async () => { return await new Promise((resolve, reject) => { var stream = createReadStream(import.meta.dir + "/readFileSync.txt", {}); @@ -1522,7 +1522,8 @@ describe.skipIf(isWindows)("createReadStream", () => { }); }); - it("works (highWaterMark 1, 512 chunk)", async () => { + // TODO - highWaterMark is just a hint, not a guarantee. it doesn't make sense to test for exact chunk sizes + it.skip("works (highWaterMark 1, 512 chunk)", async () => { var stream = createReadStream(import.meta.dir + "/readLargeFileSync.txt", { highWaterMark: 1, }); @@ -1543,7 +1544,7 @@ describe.skipIf(isWindows)("createReadStream", () => { }); }); - it("works (512 chunk)", async () => { + it.skip("works (512 chunk)", async () => { var stream = createReadStream(import.meta.dir + "/readLargeFileSync.txt", { highWaterMark: 512, }); @@ -1564,7 +1565,7 @@ describe.skipIf(isWindows)("createReadStream", () => { }); }); - it("works with larger highWaterMark (1024 chunk)", async () => { + it.skip("works with larger highWaterMark (1024 chunk)", async () => { var stream = createReadStream(import.meta.dir + "/readLargeFileSync.txt", { highWaterMark: 1024, }); diff --git a/test/js/node/process/process-sleep.js b/test/js/node/process/process-sleep.js new file mode 100644 index 00000000000000..c9178193216f8d --- /dev/null +++ b/test/js/node/process/process-sleep.js @@ -0,0 +1,3 @@ +const args = process.argv.slice(2); +const timeout = parseInt(args[0] || "0", 1); +Bun.sleepSync(timeout * 1000); diff --git a/test/js/node/process/process-stdin-echo.js b/test/js/node/process/process-stdin-echo.js index 04755862582061..77dabcb54581b8 100644 --- a/test/js/node/process/process-stdin-echo.js +++ b/test/js/node/process/process-stdin-echo.js @@ -2,10 +2,10 @@ process.stdin.setEncoding("utf8"); process.stdin.on("data", data => { process.stdout.write(data); }); -process.stdin.once(process.argv[2] == "close-event" ? "close" : "end", () => { - process.stdout.write(process.argv[2] == "close-event" ? "ENDED-CLOSE" : "ENDED"); +process.stdin.once(process.argv[2] === "close-event" ? "close" : "end", () => { + process.stdout.write(process.argv[2] === "close-event" ? "ENDED-CLOSE" : "ENDED"); }); -if (process.argv[2] == "resume") { +if (process.argv[2] === "resume") { process.stdout.write("RESUMED"); process.stdin.resume(); } diff --git a/test/js/node/process/process-stdio.test.ts b/test/js/node/process/process-stdio.test.ts index 049c94642f018a..61b5c9b53205b0 100644 --- a/test/js/node/process/process-stdio.test.ts +++ b/test/js/node/process/process-stdio.test.ts @@ -1,9 +1,8 @@ -// @known-failing-on-windows: 1 failing import { spawn, spawnSync } from "bun"; import { describe, expect, it, test } from "bun:test"; -import { bunExe } from "harness"; +import { bunEnv, bunExe } from "harness"; import { isatty } from "tty"; - +import path from "path"; test("process.stdin", () => { expect(process.stdin).toBeDefined(); expect(process.stdin.isTTY).toBe(isatty(0) ? true : undefined); @@ -11,15 +10,18 @@ test("process.stdin", () => { expect(process.stdin.once("end", function () {})).toBe(process.stdin); }); +const files = { + echo: path.join(import.meta.dir, "process-stdin-echo.js"), +}; + test("process.stdin - read", async () => { const { stdin, stdout } = spawn({ - cmd: [bunExe(), import.meta.dir + "/process-stdin-echo.js"], + cmd: [bunExe(), files.echo], stdout: "pipe", stdin: "pipe", - stderr: null, + stderr: "inherit", env: { - ...process.env, - BUN_DEBUG_QUIET_LOGS: "1", + ...bunEnv, }, }); expect(stdin).toBeDefined(); @@ -42,7 +44,7 @@ test("process.stdin - read", async () => { test("process.stdin - resume", async () => { const { stdin, stdout } = spawn({ - cmd: [bunExe(), import.meta.dir + "/process-stdin-echo.js", "resume"], + cmd: [bunExe(), files.echo, "resume"], stdout: "pipe", stdin: "pipe", stderr: null, @@ -71,7 +73,7 @@ test("process.stdin - resume", async () => { test("process.stdin - close(#6713)", async () => { const { stdin, stdout } = spawn({ - cmd: [bunExe(), import.meta.dir + "/process-stdin-echo.js", "close-event"], + cmd: [bunExe(), files.echo, "close-event"], stdout: "pipe", stdin: "pipe", stderr: null, @@ -110,7 +112,7 @@ test("process.stderr", () => { test("process.stdout - write", () => { const { stdout } = spawnSync({ - cmd: [bunExe(), import.meta.dir + "/stdio-test-instance.js"], + cmd: [bunExe(), path.join(import.meta.dir, "stdio-test-instance.js")], stdout: "pipe", stdin: null, stderr: null, @@ -125,7 +127,7 @@ test("process.stdout - write", () => { test("process.stdout - write a lot (string)", () => { const { stdout } = spawnSync({ - cmd: [bunExe(), import.meta.dir + "/stdio-test-instance-a-lot.js"], + cmd: [bunExe(), path.join(import.meta.dir, "stdio-test-instance-a-lot.js")], stdout: "pipe", stdin: null, stderr: null, @@ -143,7 +145,7 @@ test("process.stdout - write a lot (string)", () => { test("process.stdout - write a lot (bytes)", () => { const { stdout } = spawnSync({ - cmd: [bunExe(), import.meta.dir + "/stdio-test-instance-a-lot.js"], + cmd: [bunExe(), path.join(import.meta.dir, "stdio-test-instance-a-lot.js")], stdout: "pipe", stdin: null, stderr: null, diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index e8218733551b03..e16c8ecff92553 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -4,6 +4,8 @@ import { existsSync, readFileSync } from "fs"; import { bunEnv, bunExe, isWindows } from "harness"; import { basename, join, resolve } from "path"; +const process_sleep = join(import.meta.dir, "process-sleep.js"); + it("process", () => { // this property isn't implemented yet but it should at least return a string const isNode = !process.isBun; @@ -409,9 +411,9 @@ if (process.platform !== "win32") { }); } -describe.skipIf(process.platform === "win32")("signal", () => { +describe("signal", () => { const fixture = join(import.meta.dir, "./process-signal-handler.fixture.js"); - it("simple case works", async () => { + it.skipIf(isWindows)("simple case works", async () => { const child = Bun.spawn({ cmd: [bunExe(), fixture, "SIGUSR1"], env: bunEnv, @@ -420,7 +422,7 @@ describe.skipIf(process.platform === "win32")("signal", () => { expect(await child.exited).toBe(0); expect(await new Response(child.stdout).text()).toBe("PASS\n"); }); - it("process.emit will call signal events", async () => { + it.skipIf(isWindows)("process.emit will call signal events", async () => { const child = Bun.spawn({ cmd: [bunExe(), fixture, "SIGUSR2"], env: bunEnv, @@ -432,26 +434,38 @@ describe.skipIf(process.platform === "win32")("signal", () => { it("process.kill(2) works", async () => { const child = Bun.spawn({ - cmd: ["bash", "-c", "sleep 1000000"], + cmd: [bunExe(), process_sleep, "1000000"], stdout: "pipe", + env: bunEnv, }); const prom = child.exited; const ret = process.kill(child.pid, "SIGTERM"); expect(ret).toBe(true); await prom; - expect(child.signalCode).toBe("SIGTERM"); + if (process.platform === "win32") { + expect(child.exitCode).toBe(1); + } else { + expect(child.signalCode).toBe("SIGTERM"); + } }); it("process._kill(2) works", async () => { const child = Bun.spawn({ - cmd: ["bash", "-c", "sleep 1000000"], + cmd: [bunExe(), process_sleep, "1000000"], stdout: "pipe", + env: bunEnv, }); const prom = child.exited; - const ret = process.kill(child.pid, "SIGKILL"); - expect(ret).toBe(true); + // SIGKILL as a number + const SIGKILL = 9; + process._kill(child.pid, SIGKILL); await prom; - expect(child.signalCode).toBe("SIGKILL"); + + if (process.platform === "win32") { + expect(child.exitCode).toBe(1); + } else { + expect(child.signalCode).toBe("SIGKILL"); + } }); it("process.kill(2) throws on invalid input", async () => { diff --git a/test/js/node/stream/emit-readable-on-end.js b/test/js/node/stream/emit-readable-on-end.js new file mode 100644 index 00000000000000..f6f49445932f9f --- /dev/null +++ b/test/js/node/stream/emit-readable-on-end.js @@ -0,0 +1,19 @@ +const { writeFileSync, createReadStream } = require("fs"); +const { join } = require("path"); +const { tmpdir } = require("os"); + +// This test should fail if ot doesn't go through the "readable" event +process.exitCode = 1; + +const testData = new Uint8Array(parseInt(process.env.READABLE_SIZE || (1024 * 1024).toString(10))).fill("a"); +const path = join(tmpdir(), `${Date.now()}-testEmitReadableOnEnd.txt`); +writeFileSync(path, testData); + +const stream = createReadStream(path); + +stream.on("readable", () => { + const chunk = stream.read(); + if (!chunk) { + process.exitCode = 0; + } +}); diff --git a/test/js/node/stream/node-stream.test.js b/test/js/node/stream/node-stream.test.js index a8649e6c0db5a5..51544a5e79bb35 100644 --- a/test/js/node/stream/node-stream.test.js +++ b/test/js/node/stream/node-stream.test.js @@ -122,8 +122,10 @@ describe("Readable", () => { stream.pipe(writable); }); it("should be able to be piped via .pipe with a large file", done => { - const length = 128 * 1024; - const data = "B".repeat(length); + const data = Buffer.allocUnsafe(768 * 1024) + .fill("B") + .toString(); + const length = data.length; const path = `${tmpdir()}/${Date.now()}.testReadStreamLargeFile.txt`; writeFileSync(path, data); const stream = createReadStream(path, { start: 0, end: length - 1 }); @@ -186,18 +188,8 @@ describe("createReadStream", () => { }); }); - it("should emit readable on end", done => { - const testData = "Hello world"; - const path = join(tmpdir(), `${Date.now()}-testEmitReadableOnEnd.txt`); - writeFileSync(path, testData); - const stream = createReadStream(path); - - stream.on("readable", () => { - const chunk = stream.read(); - if (!chunk) { - done(); - } - }); + it("should emit readable on end", () => { + expect([join(import.meta.dir, "emit-readable-on-end.js")]).toRun(); }); }); @@ -261,24 +253,29 @@ process.stdin.pipe(transform).pipe(process.stdout); process.stdin.on("end", () => console.log(totalChunkSize)); `; describe("process.stdin", () => { - it("should pipe correctly", done => { - mkdirSync(join(tmpdir(), "process-stdin-test"), { recursive: true }); - writeFileSync(join(tmpdir(), "process-stdin-test/process-stdin.test.js"), processStdInTest, {}); + it("should pipe correctly", async () => { + const dir = join(tmpdir(), "process-stdin-test"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "process-stdin-test.js"), processStdInTest, {}); // A sufficiently large input to make at least four chunks const ARRAY_SIZE = 8_388_628; const typedArray = new Uint8Array(ARRAY_SIZE).fill(97); - const { stdout, exitCode, stderr } = Bun.spawnSync({ - cmd: [bunExe(), "test", "process-stdin.test.js"], - cwd: join(tmpdir(), "process-stdin-test"), + const { stdout, exited, stdin } = Bun.spawn({ + cmd: [bunExe(), "process-stdin-test.js"], + cwd: dir, env: bunEnv, - stdin: typedArray, + stdin: "pipe", + stdout: "pipe", + stderr: "inherit", }); - expect(exitCode).toBe(0); - expect(String(stdout)).toBe(`${ARRAY_SIZE}\n`); - done(); + stdin.write(typedArray); + await stdin.end(); + + expect(await exited).toBe(0); + expect(await new Response(stdout).text()).toBe(`${ARRAY_SIZE}\n`); }); }); diff --git a/test/js/third_party/es-module-lexer/es-module-lexer.test.ts b/test/js/third_party/es-module-lexer/es-module-lexer.test.ts index 2202a61a67b621..e9f7ebbbd6e802 100644 --- a/test/js/third_party/es-module-lexer/es-module-lexer.test.ts +++ b/test/js/third_party/es-module-lexer/es-module-lexer.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test"; -import { spawnSync } from "bun"; +import { spawn } from "bun"; import { bunEnv, bunExe } from "../../../harness"; import { join } from "path"; @@ -11,13 +11,13 @@ import { join } from "path"; // // At the time of writing, this includes WebAssembly compilation and Atomics // It excludes FinalizationRegistry since that doesn't need to keep the process alive. -test("es-module-lexer consistently loads", () => { +test("es-module-lexer consistently loads", async () => { for (let i = 0; i < 10; i++) { - const { stdout, exitCode } = spawnSync({ + const { stdout, exited } = spawn({ cmd: [bunExe(), join(import.meta.dir, "index.ts")], env: bunEnv, }); - expect(JSON.parse(stdout?.toString())).toEqual({ + expect(await new Response(stdout).json()).toEqual({ imports: [ { n: "b", @@ -40,6 +40,6 @@ test("es-module-lexer consistently loads", () => { }, ], }); - expect(exitCode).toBe(42); + expect(await exited).toBe(42); } }); diff --git a/test/js/third_party/esbuild/esbuild-child_process.test.ts b/test/js/third_party/esbuild/esbuild-child_process.test.ts index 70226c43e7cea6..9971dbf9ec0344 100644 --- a/test/js/third_party/esbuild/esbuild-child_process.test.ts +++ b/test/js/third_party/esbuild/esbuild-child_process.test.ts @@ -1,18 +1,16 @@ -// @known-failing-on-windows: 1 failing import { spawnSync } from "bun"; import { describe, it, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; test("esbuild", () => { - const { exitCode, stderr, stdout } = spawnSync([bunExe(), import.meta.dir + "/esbuild-test.js"], { + const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/esbuild-test.js"], { env: { ...bunEnv, }, + detached: true, + stdout: "inherit", + stderr: "inherit", + stdin: "inherit", }); - const out = "" + stderr?.toString() + stdout?.toString(); - if (exitCode !== 0 && out?.length) { - throw new Error(out); - } - expect(exitCode).toBe(0); }); diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index 9f3120ae6f6d8d..95e27c0521cf8b 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -1,20 +1,37 @@ import { file, spawn } from "bun"; import { expect, it } from "bun:test"; -import { bunExe } from "harness"; - +import { bunEnv, bunExe } from "harness"; +import { join } from "node:path"; it("should log to console correctly", async () => { const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), import.meta.dir + "/console-log.js"], - stdin: null, + cmd: [bunExe(), join(import.meta.dir, "console-log.js")], + stdin: "inherit", stdout: "pipe", stderr: "pipe", - env: { - BUN_DEBUG_QUIET_LOGS: "1", - }, + env: bunEnv, }); - expect(await exited).toBe(0); - expect((await new Response(stderr).text()).replaceAll("\r\n", "\n")).toBe("uh oh\n"); - expect((await new Response(stdout).text()).replaceAll("\r\n", "\n")).toBe( - (await new Response(file(import.meta.dir + "/console-log.expected.txt")).text()).replaceAll("\r\n", "\n"), + const exitCode = await exited; + const err = (await new Response(stderr).text()).replaceAll("\r\n", "\n"); + const out = (await new Response(stdout).text()).replaceAll("\r\n", "\n"); + const expected = (await new Response(file(join(import.meta.dir, "console-log.expected.txt"))).text()).replaceAll( + "\r\n", + "\n", ); + + const errMatch = err === "uh oh\n"; + const outmatch = out === expected; + + if (errMatch && outmatch && exitCode === 0) { + expect().pass(); + return; + } + + console.error(err); + console.log("Length of output:", out.length); + console.log("Length of expected:", expected.length); + console.log("Exit code:", exitCode); + + expect(out).toBe(expected); + expect(err).toBe("uh oh\n"); + expect(exitCode).toBe(0); }); diff --git a/test/js/web/console/console-timeLog.test.ts b/test/js/web/console/console-timeLog.test.ts index f17fc28f4df061..bbfe33821f8bf1 100644 --- a/test/js/web/console/console-timeLog.test.ts +++ b/test/js/web/console/console-timeLog.test.ts @@ -1,10 +1,10 @@ import { file, spawn } from "bun"; import { expect, it } from "bun:test"; import { bunExe, bunEnv } from "harness"; - +import { join } from "node:path"; it("should log to console correctly", async () => { const { stderr, exited } = spawn({ - cmd: [bunExe(), import.meta.dir + "/console-timeLog.js"], + cmd: [bunExe(), join(import.meta.dir, "console-timeLog.js")], stdin: null, stdout: "pipe", stderr: "pipe", @@ -12,6 +12,10 @@ it("should log to console correctly", async () => { }); expect(await exited).toBe(0); const outText = await new Response(stderr).text(); - const expectedText = (await file(import.meta.dir + "/console-timeLog.expected.txt").text()).replaceAll("\r\n", "\n"); + const expectedText = (await file(join(import.meta.dir, "console-timeLog.expected.txt")).text()).replaceAll( + "\r\n", + "\n", + ); + expect(outText.replace(/^\[.+?s\] /gm, "")).toBe(expectedText.replace(/^\[.+?s\] /gm, "")); }); diff --git a/test/js/web/fetch/body.test.ts b/test/js/web/fetch/body.test.ts index 2b9b3e11d512a7..4f5be656181597 100644 --- a/test/js/web/fetch/body.test.ts +++ b/test/js/web/fetch/body.test.ts @@ -169,23 +169,6 @@ for (const { body, fn } of bodyTypes) { }); describe("ReadableStream", () => { const streams = [ - { - label: "empty stream", - stream: () => new ReadableStream(), - content: "", - skip: true, // hangs - }, - { - label: "custom stream", - stream: () => - new ReadableStream({ - start(controller) { - controller.enqueue("hello\n"); - }, - }), - content: "hello\n", - skip: true, // hangs - }, { label: "direct stream", stream: () => diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index c9b1f8cea7f28a..eb1328c50f2d95 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -1234,10 +1234,10 @@ describe("Response", () => { }).toThrow("Body already used"); }); it("with Bun.file() streams", async () => { - var stream = Bun.file(import.meta.dir + "/fixtures/file.txt").stream(); + var stream = Bun.file(join(import.meta.dir, "fixtures/file.txt")).stream(); expect(stream instanceof ReadableStream).toBe(true); var input = new Response((await new Response(stream).blob()).stream()).arrayBuffer(); - var output = Bun.file(import.meta.dir + "/fixtures/file.txt").arrayBuffer(); + var output = Bun.file(join(import.meta.dir, "/fixtures/file.txt")).arrayBuffer(); expect(await input).toEqual(await output); }); it("with Bun.file() with request/response", async () => { @@ -1257,7 +1257,7 @@ describe("Response", () => { }); var input = await response.arrayBuffer(); var output = await Bun.file(import.meta.dir + "/fixtures/file.txt").stream(); - expect(input).toEqual((await output.getReader().read()).value?.buffer); + expect(new Uint8Array(input)).toEqual((await output.getReader().read()).value); }); }); diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index 605edf39d1a62d..a578123bb9f536 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { file, readableStreamToArrayBuffer, readableStreamToArray, readableStreamToText, ArrayBufferSink } from "bun"; import { expect, it, beforeEach, afterEach, describe, test } from "bun:test"; import { mkfifo } from "mkfifo"; @@ -437,7 +436,7 @@ it.skipIf(isWindows)("Bun.file() read text from pipe", async () => { const proc = Bun.spawn({ cmd: ["bash", join(import.meta.dir + "/", "bun-streams-test-fifo.sh"), "/tmp/fifo"], stderr: "inherit", - stdout: null, + stdout: "pipe", stdin: null, env: { FIFO_TEST: large, diff --git a/test/js/web/websocket/websocket.test.js b/test/js/web/websocket/websocket.test.js index e4e70bab31527d..9212655dbcbcfc 100644 --- a/test/js/web/websocket/websocket.test.js +++ b/test/js/web/websocket/websocket.test.js @@ -511,7 +511,7 @@ describe("websocket in subprocess", () => { if (isWindows) { expect(await subprocess.exited).toBe(1); } else { - expect(await subprocess.exited).toBe(129); + expect(await subprocess.exited).toBe(143); } }); diff --git a/test/regression/issue/02499.test.ts b/test/regression/issue/02499.test.ts index 1b74e644801610..a356dc9222cbf7 100644 --- a/test/regression/issue/02499.test.ts +++ b/test/regression/issue/02499.test.ts @@ -1,10 +1,7 @@ -// @known-failing-on-windows: 1 failing +import { spawn } from "bun"; import { expect, it } from "bun:test"; -import { bunExe, bunEnv } from "../../harness.js"; -import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from "fs"; -import { tmpdir } from "os"; -import { dirname, join } from "path"; -import { sleep, spawn, spawnSync, which } from "bun"; +import { join } from "path"; +import { bunEnv, bunExe } from "../../harness.js"; // https://github.com/oven-sh/bun/issues/2499 it("onAborted() and onWritable are not called after receiving an empty response body due to a promise rejection", async testDone => { diff --git a/test/regression/issue/07500.test.ts b/test/regression/issue/07500.test.ts index f18f0dd21b6173..00095e685a5165 100644 --- a/test/regression/issue/07500.test.ts +++ b/test/regression/issue/07500.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { test, expect } from "bun:test"; import { bunEnv, bunExe, isWindows } from "harness"; import { tmpdir } from "os"; @@ -11,19 +10,23 @@ test("7500 - Bun.stdin.text() doesn't read all data", async () => { .split(" ") .join("\n"); await Bun.write(filename, text); - const cat = isWindows ? "Get-Content" : "cat"; + const cat = "cat"; const bunCommand = `${bunExe()} ${join(import.meta.dir, "7500-repro-fixture.js")}`; const shellCommand = `${cat} ${filename} | ${bunCommand}`.replace(/\\/g, "\\\\"); - const cmd = isWindows ? ["pwsh.exe", `-Command { '${shellCommand}' }`] : ["bash", "-c", shellCommand]; - const proc = Bun.spawnSync({ - cmd, + const cmd = isWindows ? (["pwsh.exe", "/C", shellCommand] as const) : (["bash", "-c", shellCommand] as const); + + const proc = Bun.spawnSync(cmd, { stdin: "inherit", stdout: "pipe", stderr: "inherit", env: bunEnv, }); + if (proc.exitCode != 0) { + throw new Error(proc.stdout.toString()); + } + const output = proc.stdout.toString().replaceAll("\r\n", "\n"); if (output !== text) { expect(output).toHaveLength(text.length); diff --git a/test/regression/issue/08093.test.ts b/test/regression/issue/08093.test.ts index 3a98f8e066a9af..1be12f5602ce5a 100644 --- a/test/regression/issue/08093.test.ts +++ b/test/regression/issue/08093.test.ts @@ -42,7 +42,7 @@ it("should install vendored node_modules with hardlink", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--backend", "hardlink"], cwd: package_dir, - stdout: null, + stdout: "pipe", stdin: "pipe", stderr: "pipe", env,