From 76ced7c6ed3ebb0cbcb2e3716b14d685cc35a1fc Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 Mar 2024 19:35:34 -0700 Subject: [PATCH] WIP Make process.stdout sync on windows (#9398) * Make some things sync on windows * WIP * WIP * remove uses to uv_default_loop * remove a compiler warning on windows * edfghjk * Windows build fixes * Fixup * bundows * Add quotes * Fix --cwd arg on Windows * comment * move this up * Fix some tests * `mv` tests pass * spawn.test passes again * Allow .sh file extension for Windows * Unmark failing tests * env test pass * windows * Fix some tests * Update ProcessBindingTTYWrap.cpp * Update CMakeLists.txt * Set tmpdir * Make it 5s on windows * Fixup * Fixup * Update ProcessBindingTTYWrap.cpp --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: dave caruso --- CMakeLists.txt | 3 +- docs/project/contributing.md | 8 ++ .../bun-internal-test/src/runner.node.mjs | 17 +++ src/__global.zig | 3 + src/bun.js/bindings/BunProcess.cpp | 18 ++- src/bun.js/bindings/ProcessBindingTTYWrap.cpp | 117 +++++++++++++++-- src/bun.js/bindings/ProcessBindingUV.cpp | 8 +- src/bun.js/bindings/ZigGlobalObject.h | 11 ++ src/bun.js/bindings/bindings.cpp | 7 +- src/bun.js/bindings/bindings.zig | 6 +- .../bindings/workaround-missing-symbols.cpp | 43 ------- src/bun.js/javascript.zig | 16 ++- src/bun.js/webcore/blob.zig | 47 +++++-- src/bun.js/webcore/streams.zig | 119 ++++++++++++------ src/bun.zig | 24 ++++ src/bun_js.zig | 14 ++- src/cli.zig | 19 ++- src/deps/libuv.zig | 12 +- src/fd.zig | 18 +++ src/io/PipeReader.zig | 4 +- src/io/PipeWriter.zig | 84 ++++++++++++- src/io/source.zig | 87 +++++++------ src/js/builtins/ProcessObjectInternals.ts | 33 ++--- src/js/internal/shared.ts | 3 + src/js/node/stream.js | 2 +- src/js/node/{tty.js => tty.ts} | 14 +-- src/main.zig | 58 ++++++--- src/options.zig | 13 +- src/shell/interpreter.zig | 12 +- src/sys.zig | 4 +- src/sys_uv.zig | 4 +- src/windows.zig | 7 ++ src/windows_c.zig | 1 + test/cli/run/env.test.ts | 8 +- test/cli/run/run-process-env.test.ts | 8 +- test/js/bun/glob/scan.test.ts | 1 - test/js/bun/shell/commands/mv.test.ts | 5 +- test/js/bun/shell/commands/rm.test.ts | 1 - test/js/bun/shell/leak.test.ts | 2 - .../bun/spawn/spawn-streaming-stdout-repro.js | 1 - test/js/bun/spawn/spawn_waiter_thread.test.ts | 13 +- test/js/bun/util/mmap.test.js | 1 - test/js/node/fs/fs.test.ts | 5 + test/js/node/nodettywrap.test.ts | 30 +++-- test/js/node/process/process-stdio.test.ts | 11 +- test/js/node/stream/node-stream.test.js | 14 ++- 46 files changed, 669 insertions(+), 267 deletions(-) rename src/js/node/{tty.js => tty.ts} (95%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c60dd48b5..ad1d7e6494 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1028,7 +1028,8 @@ elseif(CMAKE_BUILD_TYPE STREQUAL "Release") set(LTO_LINK_FLAG "") if(USE_LTO) - list(APPEND LTO_FLAG "-flto=full" "-emit-llvm") + # -emit-llvm seems to not be supported or under a different name on Windows. + list(APPEND LTO_FLAG "-flto=full") list(APPEND LTO_LINK_FLAG "/LTCG") endif() diff --git a/docs/project/contributing.md b/docs/project/contributing.md index 5e0223c1a7..8df91bcca2 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -30,6 +30,12 @@ $ sudo zypper install go cmake ninja automake git rustup && rustup toolchain ins {% /codetabs %} +{% callout } + +**Note**: The Zig compiler is automatically installed and updated by the build scripts. Manual installation is not required. + +{% /callout } + Before starting, you will need to already have a release build of Bun installed, as we use our bundler to transpile and minify our code, as well as for code generation scripts. {% codetabs %} @@ -150,6 +156,8 @@ Advanced uses can pass CMake flags to customize the build. VSCode is the recommended IDE for working on Bun, as it has been configured. Once opening, you can run `Extensions: Show Recommended Extensions` to install the recommended extensions for Zig and C++. ZLS is automatically configured. +If you use a different editor, make sure that you tell ZLS to use the automatically installed Zig compiler, which is located at `./.cache/zig/zig` (`zig.exe` on Windows). + ## Code generation scripts {% callout %} diff --git a/packages/bun-internal-test/src/runner.node.mjs b/packages/bun-internal-test/src/runner.node.mjs index d0e59c6baf..ad04ce02f0 100644 --- a/packages/bun-internal-test/src/runner.node.mjs +++ b/packages/bun-internal-test/src/runner.node.mjs @@ -12,6 +12,7 @@ import PQueue from "p-queue"; const run_start = new Date(); const TIMEOUT_DURATION = 1000 * 60 * 5; const SHORT_TIMEOUT_DURATION = Math.ceil(TIMEOUT_DURATION / 5); + function defaultConcurrency() { // This causes instability due to the number of open file descriptors / sockets in some tests // Windows has higher limits @@ -30,6 +31,22 @@ let force_ram_size = Number(BigInt(nativeMemory) >> BigInt(2)) + ""; if (!(Number.isSafeInteger(force_ram_size_input) && force_ram_size_input > 0)) { force_ram_size = force_ram_size_input + ""; } +function uncygwinTempDir() { + if (process.platform === "win32") { + for (let key of ["TMPDIR", "TEMP", "TEMPDIR", "TMP"]) { + let TMPDIR = process.env[key] || ""; + if (!/^\/[a-zA-Z]\//.test(TMPDIR)) { + continue; + } + + const driveLetter = TMPDIR[1]; + TMPDIR = path.win32.normalize(`${driveLetter.toUpperCase()}:` + TMPDIR.substring(2)); + process.env[key] = TMPDIR; + } + } +} + +uncygwinTempDir(); const cwd = resolve(fileURLToPath(import.meta.url), "../../../../"); process.chdir(cwd); diff --git a/src/__global.zig b/src/__global.zig index edfe1780fd..65ee2d3723 100644 --- a/src/__global.zig +++ b/src/__global.zig @@ -99,6 +99,9 @@ pub fn exitWide(code: u32) noreturn { runExitCallbacks(); Output.flush(); std.mem.doNotOptimizeAway(&Bun__atexit); + if (Environment.isWindows) { + bun.windows.libuv.uv_library_shutdown(); + } std.c.exit(@bitCast(code)); } diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 78c9b27c7f..b014688ce0 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1644,10 +1644,14 @@ static JSValue constructProcessHrtimeObject(VM& vm, JSObject* processObject) return hrtime; } +#if OS(WINDOWS) +extern "C" void Bun__ForceFileSinkToBeSynchronousOnWindows(JSC::JSGlobalObject*, JSC::EncodedJSValue); +#endif static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int fd) { auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); + Zig::GlobalObject* globalThis = jsCast(globalObject); JSC::JSFunction* getStdioWriteStream = JSC::JSFunction::create(vm, processObjectInternalsGetStdioWriteStreamCodeGenerator(vm), globalObject); JSC::MarkedArgumentBuffer args; @@ -1668,7 +1672,19 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, int return {}; } - return result; + ASSERT_WITH_MESSAGE(JSC::isJSArray(result), "Expected an array from getStdioWriteStream"); + JSC::JSArray* resultObject = JSC::jsCast(result); + +#if OS(WINDOWS) + // Node.js docs - https://nodejs.org/api/process.html#a-note-on-process-io + // > Files: synchronous on Windows and POSIX + // > TTYs (Terminals): asynchronous on Windows, synchronous on POSIX + // > Pipes (and sockets): synchronous on Windows, asynchronous on POSIX + // > Synchronous writes avoid problems such as output written with console.log() or console.error() being unexpectedly interleaved, or not written at all if process.exit() is called before an asynchronous write completes. See process.exit() for more information. + Bun__ForceFileSinkToBeSynchronousOnWindows(globalThis, JSValue::encode(resultObject->getIndex(globalObject, 1))); +#endif + + return resultObject->getIndex(globalObject, 0); } static JSValue constructStdout(VM& vm, JSObject* processObject) diff --git a/src/bun.js/bindings/ProcessBindingTTYWrap.cpp b/src/bun.js/bindings/ProcessBindingTTYWrap.cpp index c1679315dd..e39b22ced1 100644 --- a/src/bun.js/bindings/ProcessBindingTTYWrap.cpp +++ b/src/bun.js/bindings/ProcessBindingTTYWrap.cpp @@ -1,5 +1,7 @@ +#include "mimalloc.h" #include "root.h" +#include "JavaScriptCore/JSDestructibleObject.h" #include "JavaScriptCore/ExceptionScope.h" #include "JavaScriptCore/Identifier.h" @@ -22,6 +24,45 @@ #include #include #include + +#endif + +#if OS(WINDOWS) + +namespace UV { + +class TTY { +public: + uv_tty_t handle {}; + + uv_tty_t* tty() { return &handle; } + + void close() + { + uv_close((uv_handle_t*)(tty()), [](uv_handle_t* handle) -> void { + uv_tty_t* ttyHandle = (uv_tty_t*)handle; + ptrdiff_t offset = offsetof(UV::TTY, handle); + + UV::TTY* tty = (UV::TTY*)((char*)ttyHandle - offset); + delete tty; + }); + } +}; + +} + +static uv_tty_t* getSharedHandle(int fd, uv_loop_t* loop) +{ + static thread_local uv_tty_t* sharedHandle = nullptr; + + if (!sharedHandle) { + sharedHandle = new uv_tty_t; + memset(sharedHandle, 0, sizeof(uv_tty_t)); + uv_tty_init(loop, sharedHandle, fd, 0); + } + return sharedHandle; +} + #endif namespace Bun { @@ -72,14 +113,17 @@ static bool getWindowSize(int fd, size_t* width, size_t* height) #endif } -class TTYWrapObject final : public JSC::JSNonFinalObject { +class TTYWrapObject final : public JSC::JSDestructibleObject { public: - using Base = JSC::JSNonFinalObject; + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; static TTYWrapObject* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, int fd) { + TTYWrapObject* object = new (NotNull, JSC::allocateCell(vm)) TTYWrapObject(vm, structure, fd); object->finishCreation(vm); + return object; } @@ -103,13 +147,35 @@ public: return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } + static void destroy(JSC::JSCell* cell) + { + static_cast(cell)->TTYWrapObject::~TTYWrapObject(); + } + + ~TTYWrapObject() + { +#if OS(WINDOWS) + if (handle) { + handle->close(); + } +#endif + } + int fd = -1; +#if OS(WINDOWS) + UV::TTY* handle; +#endif + private: TTYWrapObject(JSC::VM& vm, JSC::Structure* structure, const int fd) : Base(vm, structure) , fd(fd) + { +#if OS(WINDOWS) + handle = nullptr; +#endif } void finishCreation(JSC::VM& vm) @@ -147,14 +213,23 @@ JSC_DEFINE_HOST_FUNCTION(jsTTYSetMode, (JSC::JSGlobalObject * globalObject, Call } JSValue mode = callFrame->argument(1); - if (!mode.isNumber()) { - throwTypeError(globalObject, scope, "mode must be a number"_s); - return JSValue::encode(jsUndefined()); - } + auto fdToUse = fd.toInt32(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + +#if OS(WINDOWS) + if (fdToUse > -1 && fdToUse < 3) { + int err = uv_tty_set_mode(getSharedHandle(fdToUse, static_cast(globalObject)->uvLoop()), mode.isTrue() ? UV_TTY_MODE_RAW : UV_TTY_MODE_NORMAL); + return JSValue::encode(jsNumber(err)); + } else { + // TODO: + return JSValue::encode(jsNumber(0)); + } +#else // Nodejs does not throw when ttySetMode fails. An Error event is emitted instead. - int err = Bun__ttySetMode(fd.asNumber(), mode.toInt32(globalObject)); + int err = Bun__ttySetMode(fdToUse, mode.toInt32(globalObject)); return JSValue::encode(jsNumber(err)); +#endif } JSC_DEFINE_HOST_FUNCTION(TTYWrap_functionSetMode, @@ -181,8 +256,12 @@ JSC_DEFINE_HOST_FUNCTION(TTYWrap_functionSetMode, return JSValue::encode(jsUndefined()); } +#if OS(WINDOWS) + int err = uv_tty_set_mode(ttyWrap->handle->tty(), mode.toInt32(globalObject)); +#else // Nodejs does not throw when ttySetMode fails. An Error event is emitted instead. int err = Bun__ttySetMode(fd, mode.toInt32(globalObject)); +#endif return JSValue::encode(jsNumber(err)); } @@ -299,7 +378,6 @@ public: const ClassInfo TTYWrapPrototype::s_info = { "LibuvStreamWrap"_s, - &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TTYWrapPrototype) }; @@ -372,8 +450,30 @@ public: return {}; } +#if OS(WINDOWS) + auto* handle = new UV::TTY(); + memset(handle, 0, sizeof(UV::TTY)); + int rc = uv_tty_init(jsCast(globalObject)->uvLoop(), handle->tty(), fd, 0); + if (rc < 0) { + delete handle; + throwTypeError(globalObject, scope, "Failed to initialize TTY handle"_s); + return {}; + } + ASSERT(handle->tty()->loop); +#else + if (!isatty(fd)) { + throwTypeError(globalObject, scope, makeString("fd"_s, fd, " is not a tty"_s)); + return {}; + } +#endif + auto* structure = TTYWrapObject::createStructure(vm, globalObject, prototypeValue.getObject()); auto* object = TTYWrapObject::create(vm, globalObject, structure, fd); + +#if OS(WINDOWS) + object->handle = handle; +#endif + return JSValue::encode(object); } @@ -422,5 +522,4 @@ JSValue createNodeTTYWrapObject(JSC::JSGlobalObject* globalObject) return obj; } - } \ No newline at end of file diff --git a/src/bun.js/bindings/ProcessBindingUV.cpp b/src/bun.js/bindings/ProcessBindingUV.cpp index 8173072a96..3f70276521 100644 --- a/src/bun.js/bindings/ProcessBindingUV.cpp +++ b/src/bun.js/bindings/ProcessBindingUV.cpp @@ -6,7 +6,7 @@ // clang-format off -#define UV_ERRNO_MAP(macro) \ +#define BUN_UV_ERRNO_MAP(macro) \ macro(E2BIG, -7, "argument list too long") \ macro(EACCES, -13, "permission denied") \ macro(EADDRINUSE, -48, "address already in use") \ @@ -111,7 +111,7 @@ JSC_DEFINE_HOST_FUNCTION(jsErrname, (JSGlobalObject * globalObject, JSC::CallFra case value: \ return JSValue::encode(JSC::jsString(vm, String(#name##_s))); - UV_ERRNO_MAP(CASE) + BUN_UV_ERRNO_MAP(CASE) #undef CASE } @@ -131,7 +131,7 @@ JSC_DEFINE_HOST_FUNCTION(jsGetErrorMap, (JSGlobalObject * globalObject, JSC::Cal map->set(globalObject, JSC::jsNumber(value), arr); \ } - UV_ERRNO_MAP(PUT_PROPERTY) + BUN_UV_ERRNO_MAP(PUT_PROPERTY) #undef PUT_PROPERTY return JSValue::encode(map); @@ -146,7 +146,7 @@ JSObject* create(VM& vm, JSGlobalObject* globalObject) #define PUT_PROPERTY(name, value, desc) \ bindingObject->putDirect(vm, JSC::Identifier::fromString(vm, "UV_" #name##_s), JSC::jsNumber(value)); - UV_ERRNO_MAP(PUT_PROPERTY) + BUN_UV_ERRNO_MAP(PUT_PROPERTY) #undef PUT_PROPERTY bindingObject->putDirect(vm, JSC::Identifier::fromString(vm, "getErrorMap"_s), JSC::JSFunction::create(vm, globalObject, 0, "getErrorMap"_s, jsGetErrorMap, ImplementationVisibility::Public)); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 0770931692..165cd80ea5 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -55,6 +55,11 @@ class EventTarget; extern "C" void Bun__reportError(JSC__JSGlobalObject*, JSC__JSValue); extern "C" void Bun__reportUnhandledError(JSC__JSGlobalObject*, JSC::EncodedJSValue); +#if OS(WINDOWS) +#include +extern "C" uv_loop_t* Bun__ZigGlobalObject__uvLoop(void* /* BunVM */); +#endif + namespace Zig { class JSCStackTrace; @@ -300,6 +305,12 @@ public: void visitGeneratedLazyClasses(GlobalObject*, Visitor&); ALWAYS_INLINE void* bunVM() const { return m_bunVM; } +#if OS(WINDOWS) + uv_loop_t* uvLoop() const + { + return Bun__ZigGlobalObject__uvLoop(m_bunVM); + } +#endif bool isThreadLocalDefaultGlobalObject = false; JSObject* subtleCrypto() { return m_subtleCryptoObject.getInitializedOnMainThread(this); } diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index df030050d3..0d9be4c9f4 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1980,7 +1980,10 @@ CPP_DECL JSC__JSString* JSC__jsTypeStringForValue(JSC__JSGlobalObject* globalObj JSC__JSValue JSC__JSPromise__asValue(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1) { - return JSC::JSValue::encode(JSC::JSValue(arg0)); + JSValue value = JSC::JSValue(arg0); + ASSERT_WITH_MESSAGE(!value.isEmpty(), "JSPromise.asValue() called on a empty JSValue"); + ASSERT_WITH_MESSAGE(value.inherits(), "JSPromise::asValue() called on a non-promise object"); + return JSC::JSValue::encode(value); } JSC__JSPromise* JSC__JSPromise__create(JSC__JSGlobalObject* arg0) { @@ -2916,7 +2919,7 @@ void JSC__JSPromise__resolve(JSC__JSPromise* arg0, JSC__JSGlobalObject* arg1, { ASSERT_WITH_MESSAGE(arg0->inherits(), "Argument is not a promise"); ASSERT_WITH_MESSAGE(arg0->status(arg0->vm()) == JSC::JSPromise::Status::Pending, "Promise is already resolved or rejected"); - + ASSERT_WITH_MESSAGE(!JSValue::decode(JSValue2).inherits(), "Called .resolve() with another Promise. Did you mean to do that?"); arg0->resolve(arg1, JSC::JSValue::decode(JSValue2)); } diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 3cece9e210..54af05a94b 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2118,15 +2118,15 @@ pub const JSPromise = extern struct { }; } - pub fn get(this: *Strong) *JSC.JSPromise { + pub fn get(this: *const Strong) *JSC.JSPromise { return this.strong.get().?.asPromise().?; } - pub fn value(this: *Strong) JSValue { + pub fn value(this: *const Strong) JSValue { return this.strong.get().?; } - pub fn valueOrEmpty(this: *Strong) JSValue { + pub fn valueOrEmpty(this: *const Strong) JSValue { return this.strong.get() orelse .zero; } diff --git a/src/bun.js/bindings/workaround-missing-symbols.cpp b/src/bun.js/bindings/workaround-missing-symbols.cpp index f7d5c56451..d40e521f26 100644 --- a/src/bun.js/bindings/workaround-missing-symbols.cpp +++ b/src/bun.js/bindings/workaround-missing-symbols.cpp @@ -42,49 +42,6 @@ extern "C" int kill(int pid, int sig) return uv_kill(pid, sig); } -extern "C" int readlink(const char* path, char* buf, size_t bufsize) -{ - uv_fs_t req; - req.result = 0; - - int len = uv_fs_readlink(uv_default_loop(), &req, path, nullptr); - if (req.result < 0) { - uv_fs_req_cleanup(&req); - return req.result; - } - - if (bufsize > req.result) - bufsize = req.result; - - size_t outlen = std::min(static_cast(len), bufsize); - memcpy(buf, req.ptr, outlen); - uv_fs_req_cleanup(&req); - - return outlen; -} - -extern "C" int link(const char* oldpath, const char* newpath) -{ - uv_fs_t req; - int status_code = uv_fs_link(uv_default_loop(), &req, oldpath, newpath, nullptr); - uv_fs_req_cleanup(&req); - return status_code; -} - -extern "C" char* mkdtemp(char* template_name) -{ - uv_fs_t req; - int status_code = uv_fs_mkdtemp(uv_default_loop(), &req, template_name, nullptr); - - if (status_code < 0) - return nullptr; - size_t outlen = std::min(strlen(req.path), strlen(template_name)); - memcpy(template_name, req.path, outlen); - template_name[outlen] = '\0'; - uv_fs_req_cleanup(&req); - return template_name; -} - #endif #if !defined(WIN32) diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index dc445c041f..2229aff9ef 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -385,13 +385,17 @@ pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSG jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; } -// pub fn getGlobalExitCodeForPipeFailure() u8 { -// if (VirtualMachine.is_main_thread_vm) { -// return VirtualMachine.get().exit_handler.exit_code; -// } +const WindowsOnly = struct { + pub fn Bun__ZigGlobalObject__uvLoop(jsc_vm: *VirtualMachine) callconv(.C) *bun.windows.libuv.Loop { + return jsc_vm.uvLoop(); + } +}; -// return 0; -// } +comptime { + if (Environment.isWindows) { + @export(WindowsOnly.Bun__ZigGlobalObject__uvLoop, .{ .name = "Bun__ZigGlobalObject__uvLoop" }); + } +} pub const ExitHandler = struct { exit_code: u8 = 0, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 6e630aa79a..e21fd1104d 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -2943,6 +2943,7 @@ pub const Blob = struct { if (Environment.isWindows) { const pathlike = store.data.file.pathlike; + const vm = globalThis.bunVM(); const fd: bun.FileDescriptor = if (pathlike == .fd) pathlike.fd else brk: { var file_path: [bun.MAX_PATH_BYTES]u8 = undefined; switch (bun.sys.open( @@ -2961,16 +2962,48 @@ pub const Blob = struct { unreachable; }; + const is_stdout_or_stderr = brk: { + if (pathlike != .fd) { + break :brk false; + } + + if (vm.rare_data) |rare| { + if (store == rare.stdout_store) { + break :brk true; + } + + if (store == rare.stderr_store) { + break :brk true; + } + } + + break :brk switch (bun.FDTag.get(fd)) { + .stdout, .stderr => true, + else => false, + }; + }; var sink = JSC.WebCore.FileSink.init(fd, this.globalThis.bunVM().eventLoop()); - switch (sink.writer.start(fd, false)) { - .err => |err| { - globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); - sink.deref(); + if (is_stdout_or_stderr) { + switch (sink.writer.startSync(fd, false)) { + .err => |err| { + globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); + sink.deref(); - return JSC.JSValue.zero; - }, - else => {}, + return JSC.JSValue.zero; + }, + else => {}, + } + } else { + switch (sink.writer.start(fd, true)) { + .err => |err| { + globalThis.vm().throwError(globalThis, err.toJSC(globalThis)); + sink.deref(); + + return JSC.JSValue.zero; + }, + else => {}, + } } return sink.toJS(globalThis); diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index 2874ee3f79..f8fc2f6e19 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -667,30 +667,43 @@ pub const StreamResult = union(Tag) { into_array_and_done: Blob.SizeType, pub const Pending = struct { - future: Future = undefined, + future: Future = .{ .none = {} }, result: Writable, consumed: Blob.SizeType = 0, state: StreamResult.Pending.State = .none, - pub fn deinit(_: *@This()) void { - // TODO: + pub fn deinit(this: *@This()) void { + this.future.deinit(); } pub const Future = union(enum) { - promise: struct { - promise: *JSPromise, - globalThis: *JSC.JSGlobalObject, - }, + none: void, + promise: JSC.JSPromise.Strong, handler: Handler, + + pub fn deinit(this: *@This()) void { + if (this.* == .promise) { + this.promise.strong.deinit(); + this.* = .{ .none = {} }; + } + } }; pub fn promise(this: *Writable.Pending, globalThis: *JSC.JSGlobalObject) *JSPromise { - const prom = JSPromise.create(globalThis); - this.future = .{ - .promise = .{ .promise = prom, .globalThis = globalThis }, - }; this.state = .pending; - return prom; + + switch (this.future) { + .promise => |p| { + return p.get(); + }, + else => { + this.future = .{ + .promise = JSC.JSPromise.Strong.init(globalThis), + }; + + return this.future.promise.get(); + }, + } } pub const Handler = struct { @@ -714,12 +727,15 @@ pub const StreamResult = union(Tag) { if (this.state != .pending) return; this.state = .used; switch (this.future) { - .promise => |p| { - Writable.fulfillPromise(this.result, p.promise, p.globalThis); + .promise => { + var p = this.future.promise; + this.future = .none; + Writable.fulfillPromise(this.result, p.swap(), p.strong.globalThis.?); }, .handler => |h| { h.handler(h.ctx, this.result); }, + .none => {}, } } }; @@ -765,11 +781,7 @@ pub const StreamResult = union(Tag) { // undefined == noop, but we probably won't send it .done => JSC.JSValue.jsBoolean(true), - .pending => |pending| brk: { - const promise_value = pending.promise(globalThis).asValue(globalThis); - promise_value.protect(); - break :brk promise_value; - }, + .pending => |pending| pending.promise(globalThis).asValue(globalThis), }; } }; @@ -2880,6 +2892,7 @@ pub const FileSink = struct { // we should not duplicate these fields... pollable: bool = false, nonblocking: bool = false, + force_sync_on_windows: bool = false, is_socket: bool = false, fd: bun.FileDescriptor = bun.invalid_fd, has_js_called_unref: bool = false, @@ -2891,6 +2904,19 @@ pub const FileSink = struct { pub const IOWriter = bun.io.StreamingWriter(@This(), onWrite, onError, onReady, onClose); pub const Poll = IOWriter; + fn Bun__ForceFileSinkToBeSynchronousOnWindows(globalObject: *JSC.JSGlobalObject, jsvalue: JSC.JSValue) callconv(.C) void { + comptime std.debug.assert(Environment.isWindows); + + var this: *FileSink = @alignCast(@ptrCast(JSSink.fromJS(globalObject, jsvalue) orelse return)); + this.force_sync_on_windows = true; + } + + comptime { + if (Environment.isWindows) { + @export(Bun__ForceFileSinkToBeSynchronousOnWindows, .{ .name = "Bun__ForceFileSinkToBeSynchronousOnWindows" }); + } + } + pub fn onAttachedProcessExit(this: *FileSink) void { log("onAttachedProcessExit()", .{}); this.done = true; @@ -3029,7 +3055,7 @@ pub const FileSink = struct { // 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), + .fd => |fd_| bun.sys.dupWithFlags(fd_, if (bun.FDTag.get(fd_) == .none and !this.force_sync_on_windows) std.os.O.NONBLOCK else 0), }) { .err => |err| return .{ .err = err }, .result => |fd| fd, @@ -3052,12 +3078,30 @@ pub const FileSink = struct { }, } } else if (comptime Environment.isWindows) { - this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0; + this.pollable = (bun.windows.GetFileType(fd.cast()) & bun.windows.FILE_TYPE_PIPE) != 0 and !this.force_sync_on_windows; this.fd = fd; } else { @compileError("TODO: implement for this platform"); } + if (comptime Environment.isWindows) { + if (this.force_sync_on_windows) { + switch (this.writer.startSync( + fd, + this.pollable, + )) { + .err => |err| { + _ = bun.sys.close(fd); + return .{ .err = err }; + }, + .result => { + this.writer.updateRef(this.eventLoop(), false); + }, + } + return .{ .result = {} }; + } + } + switch (this.writer.start( fd, this.pollable, @@ -3125,7 +3169,7 @@ pub const FileSink = struct { pub fn flushFromJS(this: *FileSink, globalThis: *JSGlobalObject, wait: bool) JSC.Maybe(JSValue) { _ = wait; // autofix if (this.pending.state == .pending) { - return .{ .result = this.pending.future.promise.promise.asValue(globalThis) }; + return .{ .result = this.pending.future.promise.value() }; } if (this.done) { @@ -3191,6 +3235,7 @@ pub const FileSink = struct { if (this.done) { return .{ .done = {} }; } + return this.toResult(this.writer.writeLatin1(data.slice())); } pub fn writeUTF16(this: *@This(), data: StreamResult) StreamResult.Writable { @@ -3235,6 +3280,7 @@ pub const FileSink = struct { } } pub fn deinit(this: *FileSink) void { + this.pending.deinit(); this.writer.deinit(); } @@ -3249,7 +3295,7 @@ pub const FileSink = struct { 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) }; + return .{ .result = this.pending.future.promise.value() }; } return .{ .result = JSValue.jsNumber(this.written) }; @@ -3375,20 +3421,21 @@ pub const FileReader = struct { 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; - } - } + // 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; + // } + // } - break :brk 0; - })) { - .result => |_fd| if (Environment.isWindows) bun.toLibUVOwnedFD(_fd) else _fd, - .err => |err| { - return .{ .err = err.withFd(file.pathlike.fd) }; - }, - } + // break :brk 0; + // })) { + // .result => |_fd| if (Environment.isWindows) bun.toLibUVOwnedFD(_fd) else _fd, + // .err => |err| { + // return .{ .err = err.withFd(file.pathlike.fd) }; + // }, + // } + 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| { diff --git a/src/bun.zig b/src/bun.zig index 32b2216df9..e3688646ce 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1915,6 +1915,20 @@ pub inline fn uvfdcast(fd: anytype) FDImpl.UV { FileDescriptor => FDImpl.decode(fd), else => @compileError("uvfdcast() does not support type \"" ++ @typeName(T) ++ "\""), }); + + // Specifically allow these anywhere: + if (fd == win32.STDIN_FD) { + return 0; + } + + if (fd == win32.STDOUT_FD) { + return 1; + } + + if (fd == win32.STDERR_FD) { + return 2; + } + if (Environment.allow_assert) { if (decoded.kind != .uv) { std.debug.panic("uvfdcast({}) called on an windows handle", .{decoded}); @@ -2156,7 +2170,17 @@ pub const FDTag = enum { stdout, pub fn get(fd_: anytype) FDTag { const fd = toFD(fd_); + const T = @TypeOf(fd_); if (comptime Environment.isWindows) { + if (@typeInfo(T) == .Int or @typeInfo(T) == .ComptimeInt) { + switch (fd_) { + 0 => return .stdin, + 1 => return .stdout, + 2 => return .stderr, + else => {}, + } + } + if (fd == win32.STDOUT_FD) { return .stdout; } else if (fd == win32.STDERR_FD) { diff --git a/src/bun_js.zig b/src/bun_js.zig index 690eea456a..02419c4cd4 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -142,22 +142,24 @@ pub const Run = struct { pub fn boot(ctx_: Command.Context, entry_path: string) !void { var ctx = ctx_; JSC.markBinding(@src()); - bun.JSC.initialize(); - if (strings.endsWithComptime(entry_path, ".bun.sh")) { + if (!ctx.debug.loaded_bunfig) { + try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", &ctx, .RunCommand); + } + + if (strings.endsWithComptime(entry_path, comptime if (Environment.isWindows) ".sh" else ".bun.sh")) { const exit_code = try bootBunShell(&ctx, entry_path); Global.exitWide(exit_code); return; } + // The shell does not need to initialize JSC. + // JSC initialization costs 1-3ms + bun.JSC.initialize(); js_ast.Expr.Data.Store.create(default_allocator); js_ast.Stmt.Data.Store.create(default_allocator); var arena = try Arena.init(); - if (!ctx.debug.loaded_bunfig) { - try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", &ctx, .RunCommand); - } - run = .{ .vm = try VirtualMachine.init( .{ diff --git a/src/cli.zig b/src/cli.zig index 51af9f68d8..80dd184eb0 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -408,10 +408,21 @@ pub const Arguments = struct { if (args.option("--cwd")) |cwd_| { cwd = brk: { var outbuf: [bun.MAX_PATH_BYTES]u8 = undefined; - const out = std.os.realpath(cwd_, &outbuf) catch |err| { - Output.prettyErrorln("error resolving --cwd: {s}", .{@errorName(err)}); - Global.exit(1); - }; + const out = bun.path.joinAbs(try bun.getcwd(&outbuf), .loose, cwd_); + + // On POSIX, we don't actually call chdir() on the path + // + // On Windows, we do change the current directory. + // Not all system calls on Windows support passing a dirfd (and libuv entirely doesn't) + // So we have to do it the real way + if (comptime Environment.isWindows) { + var wbuf: bun.WPathBuffer = undefined; + bun.sys.chdir(bun.strings.toWPathNormalized(&wbuf, out)).unwrap() catch |err| { + Output.prettyErrorln("{}\n", .{err}); + Global.exit(1); + }; + } + break :brk try allocator.dupe(u8, out); }; } else { diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 228033dca7..8577026fff 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -1371,6 +1371,16 @@ pub const struct_uv_tty_s = extern struct { .{ .result = {} }; } + const Mode = enum(c_uint) { + normal = 0, + raw = 1, + io = 2, + }; + + pub fn setMode(this: *uv_tty_t, mode: Mode) ReturnCode { + return uv_tty_set_mode(this, @intFromEnum(mode)); + } + pub usingnamespace StreamMixin(@This()); }; pub const uv_tty_t = struct_uv_tty_s; @@ -2096,7 +2106,7 @@ 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) ReturnCode; -pub extern fn uv_tty_set_mode([*c]uv_tty_t, mode: uv_tty_mode_t) c_int; +pub extern fn uv_tty_set_mode(*uv_tty_t, mode: uv_tty_mode_t) ReturnCode; 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; pub extern fn uv_tty_set_vterm_state(state: uv_tty_vtermstate_t) void; diff --git a/src/fd.zig b/src/fd.zig index f0d516e9b5..de1e042e3c 100644 --- a/src/fd.zig +++ b/src/fd.zig @@ -289,6 +289,14 @@ pub const FDImpl = packed struct { pub fn fromJS(value: JSValue) ?FDImpl { if (!value.isInt32()) return null; const fd = value.asInt32(); + if (comptime env.isWindows) { + return switch (bun.FDTag.get(fd)) { + .stdin => FDImpl.decode(bun.STDIN_FD), + .stdout => FDImpl.decode(bun.STDOUT_FD), + .stderr => FDImpl.decode(bun.STDERR_FD), + else => FDImpl.fromUV(fd), + }; + } return FDImpl.fromUV(fd); } @@ -300,6 +308,16 @@ pub const FDImpl = packed struct { if (!JSC.Node.Valid.fileDescriptor(fd, global, exception_ref)) { return error.JSException; } + + if (comptime env.isWindows) { + return switch (bun.FDTag.get(fd)) { + .stdin => FDImpl.decode(bun.STDIN_FD), + .stdout => FDImpl.decode(bun.STDOUT_FD), + .stderr => FDImpl.decode(bun.STDERR_FD), + else => FDImpl.fromUV(fd), + }; + } + return FDImpl.fromUV(fd); } diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index 1a333dbc0e..142ec241b6 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -341,7 +341,7 @@ pub fn WindowsPipeReader( const nread_int = nread.int(); bun.sys.syslog("onStreamRead() = {d}", .{nread_int}); - //NOTE: pipes/tty need to call stopReading on errors (yeah) + // 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) @@ -453,7 +453,7 @@ pub fn WindowsPipeReader( pub fn closeImpl(this: *This, comptime callDone: bool) void { if (this.source) |source| { switch (source) { - .file => |file| { + .sync_file, .file => |file| { file.fs.deinit(); file.fs.data = file; _ = uv.uv_fs_close(uv.Loop.get(), &source.file.fs, source.file.file, @ptrCast(&onFileClose)); diff --git a/src/io/PipeWriter.zig b/src/io/PipeWriter.zig index 3485d16453..3f342bb625 100644 --- a/src/io/PipeWriter.zig +++ b/src/io/PipeWriter.zig @@ -788,7 +788,10 @@ fn BaseWindowsPipeWriter( .file => |file| { file.fs.deinit(); file.fs.data = file; - _ = uv.uv_fs_close(uv.Loop.get(), &source.file.fs, source.file.file, @ptrCast(&onFileClose)); + _ = uv.uv_fs_close(uv.Loop.get(), &file.fs, file.file, @ptrCast(&onFileClose)); + }, + .sync_file => { + // no-op }, .pipe => |pipe| { pipe.data = pipe; @@ -834,6 +837,17 @@ fn BaseWindowsPipeWriter( return this.startWithCurrentPipe(); } + pub fn startSync(this: *WindowsPipeWriter, fd: bun.FileDescriptor, _: bool) bun.JSC.Maybe(void) { + std.debug.assert(this.source == null); + const source = Source{ + .sync_file = Source.openFile(fd), + }; + source.setData(this); + this.source = source; + 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)) { @@ -935,6 +949,9 @@ pub fn WindowsBufferedWriter( const pipe = this.source orelse return; switch (pipe) { + .sync_file => { + @panic("This code path shouldn't be reached - sync_file in PipeWriter.zig"); + }, .file => |file| { this.pending_payload_size = buffer.len; file.fs.deinit(); @@ -1029,6 +1046,32 @@ pub const StreamBuffer = struct { byte_list.writeTypeAsBytesAssumeCapacity(T, data); } + pub fn writeOrFallback(this: *StreamBuffer, buffer: anytype, comptime writeFn: anytype) ![]const u8 { + if (comptime @TypeOf(writeFn) == @TypeOf(&writeLatin1) and writeFn == &writeLatin1) { + if (bun.strings.isAllASCII(buffer)) { + return 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); + + return this.list.items[this.cursor..]; + } else if (comptime @TypeOf(writeFn) == @TypeOf(&writeUTF16) and writeFn == &writeUTF16) { + 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); + + return this.list.items[this.cursor..]; + } else if (comptime @TypeOf(writeFn) == @TypeOf(&write) and writeFn == &write) { + return buffer; + } else { + @compileError("Unsupported writeFn " ++ @typeName(@TypeOf(writeFn))); + } + } + pub fn writeLatin1(this: *StreamBuffer, buffer: []const u8) !void { if (bun.strings.isAllASCII(buffer)) { return this.write(buffer); @@ -1189,6 +1232,9 @@ pub fn WindowsStreamingWriter( this.current_payload = this.outgoing; this.outgoing = temp; switch (pipe) { + .sync_file => { + @panic("sync_file pipe write should not be reachable"); + }, .file => |file| { file.fs.deinit(); file.fs.setData(this); @@ -1237,11 +1283,39 @@ pub fn WindowsStreamingWriter( return .{ .done = 0 }; } + if (this.source != null and this.source.? == .sync_file) { + defer this.outgoing.reset(); + var remain = StreamBuffer.writeOrFallback(&this.outgoing, buffer, comptime writeFn) catch { + return .{ .err = bun.sys.Error.oom }; + }; + const initial_len = remain.len; + const fd = bun.toFD(this.source.?.sync_file.file); + + while (remain.len > 0) { + switch (bun.sys.write(fd, remain)) { + .err => |err| { + return .{ .err = err }; + }, + .result => |wrote| { + remain = remain[wrote..]; + if (wrote == 0) { + break; + } + }, + } + } + + const wrote = initial_len - remain.len; + if (wrote == 0) { + return .{ .done = wrote }; + } + return .{ .wrote = wrote }; + } + 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 }; } @@ -1250,15 +1324,15 @@ pub fn WindowsStreamingWriter( } pub fn writeUTF16(this: *WindowsWriter, buf: []const u16) WriteResult { - return writeInternal(this, buf, StreamBuffer.writeUTF16); + return writeInternal(this, buf, &StreamBuffer.writeUTF16); } pub fn writeLatin1(this: *WindowsWriter, buffer: []const u8) WriteResult { - return writeInternal(this, buffer, StreamBuffer.writeLatin1); + return writeInternal(this, buffer, &StreamBuffer.writeLatin1); } pub fn write(this: *WindowsWriter, buffer: []const u8) WriteResult { - return writeInternal(this, buffer, StreamBuffer.write); + return writeInternal(this, buffer, &StreamBuffer.write); } pub fn flush(this: *WindowsWriter) WriteResult { diff --git a/src/io/source.zig b/src/io/source.zig index 44e9d2d965..edd7367327 100644 --- a/src/io/source.zig +++ b/src/io/source.zig @@ -8,9 +8,11 @@ pub const Source = union(enum) { pipe: *Pipe, tty: *Tty, file: *File, + sync_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, @@ -18,65 +20,65 @@ pub const Source = union(enum) { }; pub fn isClosed(this: Source) bool { - switch (this) { - .pipe => |pipe| return pipe.isClosed(), - .tty => |tty| return tty.isClosed(), - .file => |file| return file.file == -1, - } + return switch (this) { + .pipe => |pipe| pipe.isClosed(), + .tty => |tty| tty.isClosed(), + .sync_file, .file => |file| file.file == -1, + }; } pub fn isActive(this: Source) bool { - switch (this) { - .pipe => |pipe| return pipe.isActive(), - .tty => |tty| return tty.isActive(), - .file => return true, - } + return switch (this) { + .pipe => |pipe| pipe.isActive(), + .tty => |tty| tty.isActive(), + .sync_file, .file => true, + }; } pub fn getHandle(this: Source) *uv.Handle { - switch (this) { - .pipe => return @ptrCast(this.pipe), - .tty => return @ptrCast(this.tty), - .file => unreachable, - } + return switch (this) { + .pipe => @ptrCast(this.pipe), + .tty => @ptrCast(this.tty), + .sync_file, .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, - } + return switch (this) { + .pipe => @ptrCast(this.pipe), + .tty => @ptrCast(this.tty), + .sync_file, .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(), - } + return switch (this) { + .pipe => |pipe| pipe.fd(), + .tty => |tty| tty.fd(), + .sync_file, .file => |file| bun.FDImpl.fromUV(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, + .pipe => |pipe| pipe.data = data, + .tty => |tty| tty.data = data, + .sync_file, .file => |file| 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, - } + return switch (this) { + .pipe => |pipe| pipe.data, + .tty => |tty| tty.data, + .sync_file, .file => |file| file.fs.data, + }; } pub fn ref(this: Source) void { switch (this) { .pipe => this.pipe.ref(), .tty => this.tty.ref(), - .file => return, + .sync_file, .file => return, } } @@ -84,16 +86,16 @@ pub const Source = union(enum) { switch (this) { .pipe => this.pipe.unref(), .tty => this.tty.unref(), - .file => return, + .sync_file, .file => return, } } pub fn hasRef(this: Source) bool { - switch (this) { - .pipe => return this.pipe.hasRef(), - .tty => return this.tty.hasRef(), - .file => return false, - } + return switch (this) { + .pipe => |pipe| pipe.hasRef(), + .tty => |tty| tty.hasRef(), + .sync_file, .file => false, + }; } pub fn openPipe(loop: *uv.Loop, fd: bun.FileDescriptor) bun.JSC.Maybe(*Source.Pipe) { @@ -125,7 +127,10 @@ pub const Source = union(enum) { return switch (tty.init(loop, bun.uvfdcast(fd))) { .err => |err| .{ .err = err }, - .result => .{ .result = tty }, + .result => brk: { + _ = tty.setMode(.raw); + break :brk .{ .result = tty }; + }, }; } diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 5425ac5026..2595500d47 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -27,25 +27,19 @@ export function getStdioWriteStream(fd) { const tty = require("node:tty"); - const stream = tty.WriteStream(fd); + let stream; + if (tty.isatty(fd)) { + stream = new tty.WriteStream(fd); + process.on("SIGWINCH", () => { + stream._refreshSize(); + }); + stream._type = "tty"; + } else { + stream = new (require("node:fs").WriteStream)(fd, { autoClose: false, fd }); + stream._type = "fs"; + } - process.on("SIGWINCH", () => { - stream._refreshSize(); - }); - - if (fd === 1) { - stream.destroySoon = stream.destroy; - stream._destroy = function (err, cb) { - cb(err); - this._undestroy(); - - if (!this._writableState.emitClose) { - process.nextTick(() => { - this.emit("close"); - }); - } - }; - } else if (fd === 2) { + if (fd === 1 || fd === 2) { stream.destroySoon = stream.destroy; stream._destroy = function (err, cb) { cb(err); @@ -59,11 +53,10 @@ export function getStdioWriteStream(fd) { }; } - stream._type = "tty"; stream._isStdio = true; stream.fd = fd; - return stream; + return [stream, stream[require("internal/shared").fileSinkSymbol]]; } export function getStdinStream(fd) { diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index 825eb14a85..729e71df2a 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -42,9 +42,12 @@ function warnNotImplementedOnce(feature: string, issue?: number) { console.warn(new NotImplementedError(feature, issue)); } +const fileSinkSymbol = Symbol("fileSink"); + export default { NotImplementedError, throwNotImplemented, hideFromStack, warnNotImplementedOnce, + fileSinkSymbol, }; diff --git a/src/js/node/stream.js b/src/js/node/stream.js index b194f2155d..74f2859f4b 100644 --- a/src/js/node/stream.js +++ b/src/js/node/stream.js @@ -5499,7 +5499,7 @@ var Writable = require_writable(); var Duplex = require_duplex(); const _pathOrFdOrSink = Symbol("pathOrFdOrSink"); -const _fileSink = Symbol("fileSink"); +const { fileSinkSymbol: _fileSink } = require("internal/shared"); const _native = Symbol("native"); function NativeWritable(pathOrFdOrSink, options = {}) { diff --git a/src/js/node/tty.js b/src/js/node/tty.ts similarity index 95% rename from src/js/node/tty.js rename to src/js/node/tty.ts index abccf05070..413c32dc2e 100644 --- a/src/js/node/tty.js +++ b/src/js/node/tty.ts @@ -25,8 +25,7 @@ Object.defineProperty(ReadStream, "prototype", { const Prototype = Object.create(require("node:fs").ReadStream.prototype); Prototype.setRawMode = function (flag) { - const mode = flag ? 1 : 0; - const err = ttySetMode(this.fd, mode); + const err = ttySetMode(this.fd, !!flag); if (err) { this.emit("error", new Error("setRawMode failed with errno: " + err)); return this; @@ -102,9 +101,7 @@ function WriteStream(fd) { if (!(this instanceof WriteStream)) return new WriteStream(fd); if (fd >> 0 !== fd || fd < 0) throw new RangeError("fd must be a positive integer"); - const stream = require("node:fs").WriteStream.$call(this, "", { - fd, - }); + const stream = require("node:fs").WriteStream.$call(this, "", { fd }); stream.columns = undefined; stream.rows = undefined; @@ -139,17 +136,16 @@ Object.defineProperty(WriteStream, "prototype", { } }; - var readline = undefined; WriteStream.prototype.clearLine = function (dir, cb) { - return (readline ??= require("node:readline")).clearLine(this, dir, cb); + return require("node:readline").clearLine(this, dir, cb); }; WriteStream.prototype.clearScreenDown = function (cb) { - return (readline ??= require("node:readline")).clearScreenDown(this, cb); + return require("node:readline").clearScreenDown(this, cb); }; WriteStream.prototype.cursorTo = function (x, y, cb) { - return (readline ??= require("node:readline")).cursorTo(this, x, y, cb); + return require("node:readline").cursorTo(this, x, y, cb); }; // The `getColorDepth` API got inspired by multiple sources such as diff --git a/src/main.zig b/src/main.zig index 07075e7202..fd48f194ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,11 +23,22 @@ pub extern "C" var environ: ?*anyopaque; // TODO: when https://github.com/ziglang/zig/pull/18692 merges, use std.os.windows for this extern fn SetConsoleMode(console_handle: *anyopaque, mode: u32) u32; - +extern fn SetStdHandle(nStdHandle: u32, hHandle: *anyopaque) u32; +pub extern "kernel32" fn SetConsoleCP(wCodePageID: std.os.windows.UINT) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; pub fn main() void { const bun = @import("root").bun; const Output = bun.Output; const Environment = bun.Environment; + // This should appear before we make any calls at all to libuv. + // So it's safest to put it very early in the main function. + if (Environment.isWindows) { + _ = bun.windows.libuv.uv_replace_allocator( + @ptrCast(&bun.Mimalloc.mi_malloc), + @ptrCast(&bun.Mimalloc.mi_realloc), + @ptrCast(&bun.Mimalloc.mi_calloc), + @ptrCast(&bun.Mimalloc.mi_free), + ); + } bun.initArgv(bun.default_allocator) catch |err| { Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)}); @@ -40,9 +51,26 @@ pub fn main() void { environ = @ptrCast(std.os.environ.ptr); _environ = @ptrCast(std.os.environ.ptr); const peb = std.os.windows.peb(); - const stdout = peb.ProcessParameters.hStdOutput; - const stderr = peb.ProcessParameters.hStdError; - const stdin = peb.ProcessParameters.hStdInput; + var stdout = peb.ProcessParameters.hStdOutput; + var stderr = peb.ProcessParameters.hStdError; + var stdin = peb.ProcessParameters.hStdInput; + + const handle_identifiers = &.{ std.os.windows.STD_INPUT_HANDLE, std.os.windows.STD_OUTPUT_HANDLE, std.os.windows.STD_ERROR_HANDLE }; + const handles = &.{ &stdin, &stdout, &stderr }; + inline for (0..3) |fd_i| { + if (handles[fd_i].* == std.os.windows.INVALID_HANDLE_VALUE) { + handles[fd_i].* = bun.windows.CreateFileW( + comptime bun.strings.w("NUL" ++ .{0}).ptr, + if (fd_i > 0) std.os.windows.GENERIC_WRITE else std.os.windows.GENERIC_READ, + 0, + null, + std.os.windows.OPEN_EXISTING, + 0, + null, + ); + _ = SetStdHandle(handle_identifiers[fd_i], handles[fd_i].*); + } + } bun.win32.STDERR_FD = if (stderr != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stderr) else bun.invalid_fd; bun.win32.STDOUT_FD = if (stdout != std.os.windows.INVALID_HANDLE_VALUE) bun.toFD(stdout) else bun.invalid_fd; @@ -55,10 +83,21 @@ pub fn main() void { // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp const CP_UTF8 = 65001; _ = w.kernel32.SetConsoleOutputCP(CP_UTF8); + _ = SetConsoleCP(CP_UTF8); + const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200; + const ENABLE_PROCESSED_OUTPUT = 0x0001; var mode: w.DWORD = undefined; if (w.kernel32.GetConsoleMode(stdout, &mode) != 0) { - _ = SetConsoleMode(stdout, mode | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING); + _ = SetConsoleMode(stdout, mode | ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + } + + if (w.kernel32.GetConsoleMode(stderr, &mode) != 0) { + _ = SetConsoleMode(stderr, mode | ENABLE_PROCESSED_OUTPUT | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING | 0); + } + + if (w.kernel32.GetConsoleMode(stdin, &mode) != 0) { + _ = SetConsoleMode(stdin, mode | ENABLE_VIRTUAL_TERMINAL_INPUT); } } @@ -79,14 +118,5 @@ pub fn main() void { bun_warn_avx_missing(@import("./cli/upgrade_command.zig").Version.Bun__githubBaselineURL.ptr); } - if (Environment.isWindows) { - _ = bun.windows.libuv.uv_replace_allocator( - @ptrCast(&bun.Mimalloc.mi_malloc), - @ptrCast(&bun.Mimalloc.mi_realloc), - @ptrCast(&bun.Mimalloc.mi_calloc), - @ptrCast(&bun.Mimalloc.mi_free), - ); - } - bun.CLI.Cli.start(bun.default_allocator, MainPanicHandler); } diff --git a/src/options.zig b/src/options.zig index 309aad9141..2188203dec 100644 --- a/src/options.zig +++ b/src/options.zig @@ -875,7 +875,7 @@ pub const Loader = enum(u8) { } }; -pub const defaultLoaders = ComptimeStringMap(Loader, .{ +const default_loaders_posix = .{ .{ ".jsx", Loader.jsx }, .{ ".json", Loader.json }, .{ ".js", Loader.jsx }, @@ -895,7 +895,16 @@ pub const defaultLoaders = ComptimeStringMap(Loader, .{ .{ ".node", Loader.napi }, .{ ".txt", Loader.text }, .{ ".text", Loader.text }, -}); +}; +const default_loaders_win32 = default_loaders_posix ++ .{ + .{ ".sh", Loader.bunsh }, +}; + +const default_loaders = if (Environment.isWindows) default_loaders_win32 else default_loaders_posix; +pub const defaultLoaders = ComptimeStringMap( + Loader, + default_loaders, +); // https://webpack.js.org/guides/package-exports/#reference-syntax pub const ESMConditions = struct { diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index a65fa4a048..3e1a5a7bde 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -6929,15 +6929,13 @@ pub const Interpreter = struct { } 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]); - - const path_in_dir = std.fs.path.joinZ(fixed_alloc.allocator(), &[_][]const u8{ - "./", - ResolvePath.basename(src), - }) catch { + const path_in_dir_ = bun.path.normalizeBuf(ResolvePath.basename(src), buf, .auto); + if (path_in_dir_.len + 1 >= buf.len) { this.err = Syscall.Error.fromCode(bun.C.E.NAMETOOLONG, .rename); return false; - }; + } + buf[path_in_dir_.len] = 0; + const path_in_dir = buf[0..path_in_dir_.len :0]; switch (Syscall.renameat(this.cwd, src, this.target_fd.?, path_in_dir)) { .err => |e| { diff --git a/src/sys.zig b/src/sys.zig index 71c0a5402e..6868ffa07e 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -1513,12 +1513,14 @@ pub fn renameat(from_dir: bun.FileDescriptor, from: [:0]const u8, to_dir: bun.Fi if (Environment.isWindows) { var w_buf_from: bun.WPathBuffer = undefined; var w_buf_to: bun.WPathBuffer = undefined; + return bun.C.renameAtW( from_dir, bun.strings.toWPath(&w_buf_from, from), to_dir, bun.strings.toWPath(&w_buf_to, to), - false, + // @paperdave why waas this set to false? + true, ); } while (true) { diff --git a/src/sys_uv.zig b/src/sys_uv.zig index 4b50b41a33..86bb9eedb0 100644 --- a/src/sys_uv.zig +++ b/src/sys_uv.zig @@ -350,9 +350,9 @@ pub fn preadv(fd: FileDescriptor, bufs: []const bun.PlatformIOVec, position: i64 return .{ .result = @as(usize, @intCast(rc.int())) }; } } - pub fn pwritev(fd: FileDescriptor, bufs: []const bun.PlatformIOVecConst, position: i64) Maybe(usize) { - const uv_fd = bun.uvfdcast(fd); + // TODO: @paperdave is this bad? + const uv_fd = bun.uvfdcast(bun.toLibUVOwnedFD(fd)); comptime std.debug.assert(bun.PlatformIOVec == uv.uv_buf_t); const debug_timer = bun.Output.DebugTimer.start(); diff --git a/src/windows.zig b/src/windows.zig index c10710c2d2..69c4f027d7 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -3020,6 +3020,13 @@ pub fn translateNTStatusToErrno(err: win32.NTSTATUS) bun.C.E { .FILE_IS_A_DIRECTORY => .ISDIR, .OBJECT_PATH_NOT_FOUND => .NOENT, .OBJECT_NAME_NOT_FOUND => .NOENT, + .NOT_A_DIRECTORY => .NOTDIR, + .RETRY => .AGAIN, + .FILE_TOO_LARGE => .@"2BIG", + .OBJECT_NAME_INVALID => if (comptime Environment.isDebug) brk: { + bun.Output.debugWarn("Received OBJECT_NAME_INVALID, indicates a file path conversion issue.", .{}); + break :brk .INVAL; + } else .INVAL, else => |t| { // if (bun.Environment.isDebug) { diff --git a/src/windows_c.zig b/src/windows_c.zig index bfb6e77fcb..1ea0fbcc4d 100644 --- a/src/windows_c.zig +++ b/src/windows_c.zig @@ -1275,6 +1275,7 @@ pub fn renameAtW( std.debug.assert(!std.fs.path.isAbsoluteWindowsWTF16(new_path_w)); } } + const src_fd = switch (bun.sys.openFileAtWindows( old_dir_fd, old_path_w, diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index 5d9582db1a..c1fa2fd1a3 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -261,7 +261,7 @@ describe("package scripts load from .env.production and .env.development", () => "name": "foo", "version": "2.0", "scripts": { - "test": `${bunExe()} run index.ts`, + "test": `'${bunExe()}' run index.ts`, }, }; const dir = tempDirWithFiles("dotenv-package-script-prod", { @@ -279,7 +279,7 @@ describe("package scripts load from .env.production and .env.development", () => "name": "foo", "version": "2.0", "scripts": { - "test": `${bunExe()} run index.ts`, + "test": `'${bunExe()}' run index.ts`, }, }; const dir = tempDirWithFiles("dotenv-package-script-prod", { @@ -701,7 +701,9 @@ console.log(dynamic().NODE_ENV); test("NODE_ENV default is not propogated in bun run", () => { const getenv = - process.platform !== "win32" ? "env | grep NODE_ENV && exit 1 || true" : "node -e if(process.env.NODE_ENV)throw(1)"; + process.platform !== "win32" + ? "env | grep NODE_ENV && exit 1 || true" + : "node -e 'if(process.env.NODE_ENV)throw(1)'"; const tmp = tempDirWithFiles("default-node-env", { "package.json": '{"scripts":{"show-env":' + JSON.stringify(getenv) + "}}", }); diff --git a/test/cli/run/run-process-env.test.ts b/test/cli/run/run-process-env.test.ts index 44bc169824..3606ee451c 100644 --- a/test/cli/run/run-process-env.test.ts +++ b/test/cli/run/run-process-env.test.ts @@ -6,10 +6,9 @@ describe("process.env", () => { const scriptName = "start:dev"; const dir = tempDirWithFiles("processenv", { - "package.json": JSON.stringify({ "scripts": { [`${scriptName}`]: `${bunExe()} run index.ts` } }), + "package.json": JSON.stringify({ "scripts": { [`${scriptName}`]: `'${bunExe()}' run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }); - const { stdout } = bunRunAsScript(dir, scriptName); expect(stdout).toBe(scriptName); }); @@ -17,13 +16,12 @@ describe("process.env", () => { // https://github.com/oven-sh/bun/issues/3589 test("npm_lifecycle_event should have the value of the last call", () => { const dir = tempDirWithFiles("processenv_ls_call", { - "package.json": JSON.stringify({ scripts: { first: `${bunExe()} run --cwd lsc second` } }), + "package.json": JSON.stringify({ scripts: { first: `'${bunExe()}' run --cwd lsc second` } }), "lsc": { - "package.json": JSON.stringify({ scripts: { second: `${bunExe()} run index.ts` } }), + "package.json": JSON.stringify({ scripts: { second: `'${bunExe()}' run index.ts` } }), "index.ts": "console.log(process.env.npm_lifecycle_event);", }, }); - const { stdout } = bunRunAsScript(dir, "first"); expect(stdout).toBe("second"); }); diff --git a/test/js/bun/glob/scan.test.ts b/test/js/bun/glob/scan.test.ts index 7700ffbe26..33784723f7 100644 --- a/test/js/bun/glob/scan.test.ts +++ b/test/js/bun/glob/scan.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing // Portions of this file are derived from works under the MIT License: // // Copyright (c) Denis Malinochkin diff --git a/test/js/bun/shell/commands/mv.test.ts b/test/js/bun/shell/commands/mv.test.ts index 4956e533eb..b2d2d4f8dd 100644 --- a/test/js/bun/shell/commands/mv.test.ts +++ b/test/js/bun/shell/commands/mv.test.ts @@ -2,6 +2,7 @@ import { $ } from "bun"; import { describe, test, expect } from "bun:test"; import { TestBuilder } from "../test_builder"; import { sortedShellOutput } from "../util"; +import { join } from "path"; describe("mv", async () => { TestBuilder.command`echo foo > a; mv a b`.ensureTempDir().fileEquals("b", "foo\n").runAsTest("move file -> file"); @@ -34,7 +35,9 @@ describe("mv", async () => { TestBuilder.command`mkdir -p foo; mkdir -p bar; echo hi > foo/inside_foo; echo hi > bar/inside_bar; mv foo bar; ls -R bar` .ensureTempDir() .stdout(str => - expect(sortedShellOutput(str)).toEqual(sortedShellOutput(["inside_bar", "foo", "bar/foo:", "inside_foo"])), + expect(sortedShellOutput(str)).toEqual( + sortedShellOutput(["inside_bar", "foo", join("bar", "foo") + ":", "inside_foo"]), + ), ) .runAsTest("move dir -> dir"); diff --git a/test/js/bun/shell/commands/rm.test.ts b/test/js/bun/shell/commands/rm.test.ts index 7272fa6788..c282337206 100644 --- a/test/js/bun/shell/commands/rm.test.ts +++ b/test/js/bun/shell/commands/rm.test.ts @@ -1,4 +1,3 @@ -// @known-failing-on-windows: panic "invalid enum value" /** * These tests are derived from the [deno_task_shell](https://github.com/denoland/deno_task_shell/) rm tests, which are developed and maintained by the Deno authors. * Copyright 2018-2023 the Deno authors. diff --git a/test/js/bun/shell/leak.test.ts b/test/js/bun/shell/leak.test.ts index 3b9c0be44b..d1274fc2f9 100644 --- a/test/js/bun/shell/leak.test.ts +++ b/test/js/bun/shell/leak.test.ts @@ -1,5 +1,3 @@ -// @known-failing-on-windows: panic "TODO on Windows" - import { $ } from "bun"; import { describe, expect, test } from "bun:test"; import { bunEnv } from "harness"; diff --git a/test/js/bun/spawn/spawn-streaming-stdout-repro.js b/test/js/bun/spawn/spawn-streaming-stdout-repro.js index 3976ff095e..eacf0f67a1 100644 --- a/test/js/bun/spawn/spawn-streaming-stdout-repro.js +++ b/test/js/bun/spawn/spawn-streaming-stdout-repro.js @@ -1,5 +1,4 @@ var writer = Bun.stdout.writer(); setInterval(() => { writer.write("Wrote to stdout\n"); - writer.flush(); }, 20); diff --git a/test/js/bun/spawn/spawn_waiter_thread.test.ts b/test/js/bun/spawn/spawn_waiter_thread.test.ts index cfaf72c046..cd89379b2f 100644 --- a/test/js/bun/spawn/spawn_waiter_thread.test.ts +++ b/test/js/bun/spawn/spawn_waiter_thread.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test"; import { spawn } from "bun"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isWindows } from "harness"; import { join } from "path"; async function run(withWaiterThread: boolean) { @@ -17,9 +17,12 @@ async function run(withWaiterThread: boolean) { cmd: [bunExe(), join(__dirname, "spawn_waiter_thread-fixture.js")], }); - setTimeout(() => { - proc.kill(process.platform !== "win32" ? "SIGKILL" : undefined); - }, 1000).unref(); + setTimeout( + () => { + proc.kill(process.platform !== "win32" ? "SIGKILL" : undefined); + }, + isWindows ? 5000 : 1000, + ).unref(); await proc.exited; @@ -27,7 +30,7 @@ async function run(withWaiterThread: boolean) { // Assert we didn't use 100% of CPU time console.log(resourceUsage.cpuTime); - expect(resourceUsage?.cpuTime.total).toBeLessThan(750_000n); + expect(resourceUsage?.cpuTime.total).toBeLessThan(750_000n * (isWindows ? 5n : 1n)); } test("issue #9404", async () => { diff --git a/test/js/bun/util/mmap.test.js b/test/js/bun/util/mmap.test.js index dae1b8de1b..f312fab1f3 100644 --- a/test/js/bun/util/mmap.test.js +++ b/test/js/bun/util/mmap.test.js @@ -1,4 +1,3 @@ -// @known-failing-on-windows: 1 failing import { describe, it, expect } from "bun:test"; import { gcTick, isWindows } from "harness"; diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 729600bf5f..f8208405c5 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -60,6 +60,11 @@ function mkdirForce(path: string) { if (!existsSync(path)) mkdirSync(path, { recursive: true }); } +it("writing to 1, 2 are possible", () => { + expect(fs.writeSync(1, Buffer.from("\nhello-stdout-test\n"))).toBe(19); + expect(fs.writeSync(2, Buffer.from("\nhello-stderr-test\n"))).toBe(19); +}); + describe("FileHandle", () => { it("FileHandle#read returns object", async () => { await using fd = await fs.promises.open(__filename); diff --git a/test/js/node/nodettywrap.test.ts b/test/js/node/nodettywrap.test.ts index aee6dc549d..399e04622d 100644 --- a/test/js/node/nodettywrap.test.ts +++ b/test/js/node/nodettywrap.test.ts @@ -27,22 +27,28 @@ test("process.binding('tty_wrap')", () => { expect(tty_isTTY(9999999)).toBe(false); expect(() => tty()).toThrow(TypeError); - expect(() => new tty(0)).not.toThrow(); - const array = [-1, -1]; + if (isatty(0)) { + expect(() => new tty(0)).not.toThrow(); - expect(() => tty_prototype.getWindowSize.call(array, 0)).toThrow(TypeError); - const ttywrapper = new tty(0); + const array = [-1, -1]; - expect(ttywrapper.getWindowSize(array)).toBeBoolean(); + expect(() => tty_prototype.getWindowSize.call(array, 0)).toThrow(TypeError); + const ttywrapper = new tty(0); - if (ttywrapper.getWindowSize(array)) { - expect(array[0]).toBeNumber(); - expect(array[0]).toBeGreaterThanOrEqual(0); - expect(array[1]).toBeNumber(); - expect(array[1]).toBeGreaterThanOrEqual(0); + expect(ttywrapper.getWindowSize(array)).toBeBoolean(); + + if (ttywrapper.getWindowSize(array)) { + expect(array[0]).toBeNumber(); + expect(array[0]).toBeGreaterThanOrEqual(0); + expect(array[1]).toBeNumber(); + expect(array[1]).toBeGreaterThanOrEqual(0); + } else { + expect(array[0]).toBe(-1); + expect(array[1]).toBe(-1); + } } else { - expect(array[0]).toBe(-1); - expect(array[1]).toBe(-1); + expect(() => new tty(0)).toThrow(); + console.warn("warn: Skipping tty tests because stdin is not a tty"); } }); diff --git a/test/js/node/process/process-stdio.test.ts b/test/js/node/process/process-stdio.test.ts index 61b5c9b532..dd1c50b3fb 100644 --- a/test/js/node/process/process-stdio.test.ts +++ b/test/js/node/process/process-stdio.test.ts @@ -48,10 +48,7 @@ test("process.stdin - resume", async () => { stdout: "pipe", stdin: "pipe", stderr: null, - env: { - ...process.env, - BUN_DEBUG_QUIET_LOGS: "1", - }, + env: bunEnv, }); expect(stdin).toBeDefined(); expect(stdout).toBeDefined(); @@ -102,12 +99,14 @@ test("process.stdin - close(#6713)", async () => { test("process.stdout", () => { expect(process.stdout).toBeDefined(); - expect(process.stdout.isTTY).toBe(isatty(1)); + // isTTY returns true or undefined in Node.js + expect(process.stdout.isTTY).toBe((isatty(1) || undefined) as any); }); test("process.stderr", () => { expect(process.stderr).toBeDefined(); - expect(process.stderr.isTTY).toBe(isatty(2)); + // isTTY returns true or undefined in Node.js + expect(process.stderr.isTTY).toBe((isatty(2) || undefined) as any); }); test("process.stdout - write", () => { diff --git a/test/js/node/stream/node-stream.test.js b/test/js/node/stream/node-stream.test.js index 51544a5e79..73ac3f9684 100644 --- a/test/js/node/stream/node-stream.test.js +++ b/test/js/node/stream/node-stream.test.js @@ -368,8 +368,18 @@ describe("TTY", () => { expect(process.stdout instanceof tty.WriteStream).toBe(true); expect(process.stderr instanceof tty.WriteStream).toBe(true); expect(process.stdin.isTTY).toBeUndefined(); - expect(process.stdout.isTTY).toBeDefined(); - expect(process.stderr.isTTY).toBeDefined(); + + if (tty.isatty(1)) { + expect(process.stdout.isTTY).toBeDefined(); + } else { + expect(process.stdout.isTTY).toBeUndefined(); + } + + if (tty.isatty(2)) { + expect(process.stderr.isTTY).toBeDefined(); + } else { + expect(process.stderr.isTTY).toBeUndefined(); + } }); it("read and write stream prototypes", () => { expect(tty.ReadStream.prototype.setRawMode).toBeInstanceOf(Function);