diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/event_loop.zig | 29 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 7 | ||||
-rw-r--r-- | src/bun.js/module_loader.zig | 69 | ||||
-rw-r--r-- | src/bun.zig | 76 | ||||
-rw-r--r-- | src/bun_js.zig | 38 | ||||
-rw-r--r-- | src/bundler.zig | 7 | ||||
-rw-r--r-- | src/cli.zig | 16 | ||||
-rw-r--r-- | src/cli/test_command.zig | 16 | ||||
-rw-r--r-- | src/install/install.zig | 4 | ||||
-rw-r--r-- | src/install/lockfile.zig | 3 | ||||
-rw-r--r-- | src/output.zig | 7 | ||||
-rw-r--r-- | src/panic_handler.zig | 9 | ||||
-rw-r--r-- | src/report.zig | 9 |
13 files changed, 244 insertions, 46 deletions
diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 8dbf5abfd..6928cd2b8 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -363,6 +363,7 @@ pub const EventLoop = struct { waker: ?AsyncIO.Waker = null, start_server_on_next_tick: bool = false, defer_count: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), + forever_timer: ?*uws.Timer = null, pub const Queue = std.fifo.LinearFifo(Task, .Dynamic); @@ -498,6 +499,34 @@ pub const EventLoop = struct { } } + pub fn tickPossiblyForever(this: *EventLoop) void { + var ctx = this.virtual_machine; + var loop = ctx.uws_event_loop.?; + + const pending_unref = ctx.pending_unref_counter; + if (pending_unref > 0) { + ctx.pending_unref_counter = 0; + loop.unrefCount(pending_unref); + } + + if (loop.num_polls == 0 or loop.active == 0) { + if (this.forever_timer == null) { + var t = uws.Timer.create(loop, this); + t.set(this, &noopForeverTimer, 1000 * 60 * 4, 1000 * 60 * 4); + this.forever_timer = t; + } + } + + loop.tick(); + this.processGCTimer(); + this.tickConcurrent(); + this.tick(); + } + + fn noopForeverTimer(_: *uws.Timer) callconv(.C) void { + // do nothing + } + pub fn autoTickActive(this: *EventLoop) void { var loop = this.virtual_machine.uws_event_loop.?; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 3841a07e2..39cead99f 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -364,6 +364,8 @@ pub const VirtualMachine = struct { preload: []const string = &[_][]const u8{}, unhandled_pending_rejection_to_capture: ?*JSC.JSValue = null, + hot_reload: bun.CLI.Command.HotReload = .none, + /// hide bun:wrap from stack traces /// bun:wrap is very noisy hide_bun_stackframes: bool = true, @@ -552,6 +554,11 @@ pub const VirtualMachine = struct { pub fn reload(this: *VirtualMachine) void { Output.debug("Reloading...", .{}); + if (this.hot_reload == .watch) { + Output.flush(); + bun.reloadProcess(bun.default_allocator, !strings.eqlComptime(this.bundler.env.map.get("BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD") orelse "0", "true")); + } + this.global.reload(); this.pending_internal_promise = this.reloadEntryPoint(this.main) catch @panic("Failed to reload"); } diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index adfa88cb1..e9a4bb2c2 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -931,13 +931,14 @@ pub const ModuleLoader = struct { jsc_vm.bundler.options.macro_remap; var fallback_source: logger.Source = undefined; - + var input_file_fd: StoredFileDescriptorType = 0; var parse_options = Bundler.ParseOptions{ .allocator = allocator, .path = path, .loader = loader, .dirname_fd = 0, .file_descriptor = fd, + .file_fd_ptr = &input_file_fd, .file_hash = hash, .macro_remappings = macro_remappings, .jsx = jsc_vm.bundler.options.jsx, @@ -962,6 +963,24 @@ pub const ModuleLoader = struct { null, disable_transpilying, ) orelse { + if (comptime !disable_transpilying) { + if (jsc_vm.isWatcherEnabled()) { + if (input_file_fd != 0) { + if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + jsc_vm.bun_watcher.?.addFile( + input_file_fd, + path.text, + hash, + loader, + 0, + package_json, + true, + ) catch {}; + } + } + } + } + return error.ParseError; }; @@ -984,6 +1003,24 @@ pub const ModuleLoader = struct { return wasm_result; } + if (comptime !disable_transpilying) { + if (jsc_vm.isWatcherEnabled()) { + if (input_file_fd != 0) { + if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { + jsc_vm.bun_watcher.?.addFile( + input_file_fd, + path.text, + hash, + loader, + 0, + package_json, + true, + ) catch {}; + } + } + } + } + if (jsc_vm.bundler.log.errors > 0) { return error.ParseError; } @@ -1026,22 +1063,6 @@ pub const ModuleLoader = struct { return error.UnexpectedPendingResolution; } - if (jsc_vm.isWatcherEnabled()) { - if (parse_result.input_fd) |fd_| { - if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { - jsc_vm.bun_watcher.?.addFile( - fd_, - path.text, - hash, - loader, - 0, - package_json, - true, - ) catch {}; - } - } - } - if (parse_result.source.contents_is_recycled) { // this shared buffer is about to become owned by the AsyncModule struct jsc_vm.bundler.resolver.caches.fs.resetSharedBuffer( @@ -1110,20 +1131,6 @@ pub const ModuleLoader = struct { if (jsc_vm.isWatcherEnabled()) { const resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, display_specifier, path.text, null); - if (parse_result.input_fd) |fd_| { - if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) { - jsc_vm.bun_watcher.?.addFile( - fd_, - path.text, - hash, - loader, - 0, - package_json, - true, - ) catch {}; - } - } - return resolved_source; } diff --git a/src/bun.zig b/src/bun.zig index d3b9a04c5..4a3dad9aa 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1016,3 +1016,79 @@ pub fn asByteSlice(buffer: anytype) []const u8 { }, }; } + +/// Reload Bun's process +/// +/// This clones envp, argv, and gets the current executable path +/// +/// Overwrites the current process with the new process +/// +/// Must be able to allocate memory. malloc is not signal safe, but it's +/// best-effort. Not much we can do if it fails. +pub fn reloadProcess( + allocator: std.mem.Allocator, + clear_terminal: bool, +) void { + const PosixSpawn = @import("./bun.js/api/bun/spawn.zig").PosixSpawn; + + var dupe_argv = allocator.allocSentinel(?[*:0]const u8, std.os.argv.len, null) catch unreachable; + for (std.os.argv, dupe_argv) |src, *dest| { + dest.* = (allocator.dupeZ(u8, sliceTo(src, 0)) catch unreachable).ptr; + } + + var environ_slice = std.mem.span(std.c.environ); + var environ = allocator.allocSentinel(?[*:0]const u8, environ_slice.len, null) catch unreachable; + for (environ_slice, environ) |src, *dest| { + if (src == null) { + dest.* = null; + } else { + dest.* = (allocator.dupeZ(u8, sliceTo(src.?, 0)) catch unreachable).ptr; + } + } + + // we must clone selfExePath incase the argv[0] was not an absolute path (what appears in the terminal) + const exec_path = (allocator.dupeZ(u8, std.fs.selfExePathAlloc(allocator) catch unreachable) catch unreachable).ptr; + + // we clone argv so that the memory address isn't the same as the libc one + const argv = @ptrCast([*:null]?[*:0]const u8, dupe_argv.ptr); + + // we clone envp so that the memory address of environment variables isn't the same as the libc one + const envp = @ptrCast([*:null]?[*:0]const u8, environ.ptr); + + // Clear the terminal + if (clear_terminal) { + Output.resetTerminalAll(); + } + + // macOS doesn't have CLOEXEC, so we must go through posix_spawn + if (comptime Environment.isMac) { + var actions = PosixSpawn.Actions.init() catch unreachable; + actions.inherit(0) catch unreachable; + actions.inherit(1) catch unreachable; + actions.inherit(2) catch unreachable; + var attrs = PosixSpawn.Attr.init() catch unreachable; + attrs.set( + C.POSIX_SPAWN_CLOEXEC_DEFAULT | + // Apple Extension: If this bit is set, rather + // than returning to the caller, posix_spawn(2) + // and posix_spawnp(2) will behave as a more + // featureful execve(2). + C.POSIX_SPAWN_SETEXEC | + C.POSIX_SPAWN_SETSIGDEF | C.POSIX_SPAWN_SETSIGMASK, + ) catch unreachable; + switch (PosixSpawn.spawnZ(exec_path, actions, attrs, @ptrCast([*:null]?[*:0]const u8, argv), @ptrCast([*:null]?[*:0]const u8, envp))) { + .err => |err| { + Output.panic("Unexpected error while reloading: {d} {s}", .{ err.errno, @tagName(err.getErrno()) }); + }, + .result => |_| {}, + } + } else { + const err = std.os.execveZ( + exec_path, + argv, + envp, + ); + Output.panic("Unexpected error while reloading: {s}", .{@errorName(err)}); + } +} +pub var auto_reload_on_crash = false; diff --git a/src/bun_js.zig b/src/bun_js.zig index f13568390..51764231e 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -147,13 +147,20 @@ pub const Run = struct { pub fn start(this: *Run) void { var vm = this.vm; - if (this.ctx.debug.hot_reload) { + vm.hot_reload = this.ctx.debug.hot_reload; + if (this.ctx.debug.hot_reload != .none) { JSC.HotReloader.enableHotModuleReloading(vm); } if (vm.loadEntryPoint(this.entry_path)) |promise| { if (promise.status(vm.global.vm()) == .Rejected) { vm.runErrorHandler(promise.result(vm.global.vm()), null); - Global.exit(1); + + if (vm.hot_reload != .none) { + vm.eventLoop().tick(); + vm.eventLoop().tickPossiblyForever(); + } else { + Global.exit(1); + } } _ = promise.result(vm.global.vm()); @@ -178,7 +185,13 @@ pub const Run = struct { } else { Output.prettyErrorln("Error occurred loading entry point: {s}", .{@errorName(err)}); } - Global.exit(1); + + if (vm.hot_reload != .none) { + vm.eventLoop().tick(); + vm.eventLoop().tickPossiblyForever(); + } else { + Global.exit(1); + } } // don't run the GC if we don't actually need to @@ -199,17 +212,26 @@ pub const Run = struct { vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); } - while (vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or vm.uws_event_loop.?.active > 0) { - vm.tick(); + while (true) { + while (vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or vm.uws_event_loop.?.active > 0) { + vm.tick(); + + // Report exceptions in hot-reloaded modules + if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { + prev_promise = this.vm.pending_internal_promise; + vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); + continue; + } + + vm.eventLoop().autoTickActive(); + } - // Report exceptions in hot-reloaded modules if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { prev_promise = this.vm.pending_internal_promise; vm.onUnhandledError(this.vm.global, this.vm.pending_internal_promise.result(vm.global.vm())); - continue; } - vm.eventLoop().autoTickActive(); + vm.eventLoop().tickPossiblyForever(); } if (this.vm.pending_internal_promise.status(vm.global.vm()) == .Rejected and prev_promise != this.vm.pending_internal_promise) { diff --git a/src/bundler.zig b/src/bundler.zig index a43d29c54..e1701ecd7 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -1324,6 +1324,10 @@ pub const Bundler = struct { dirname_fd: StoredFileDescriptorType, file_descriptor: ?StoredFileDescriptorType = null, file_hash: ?u32 = null, + + /// On exception, we might still want to watch the file. + file_fd_ptr: ?*StoredFileDescriptorType = null, + path: Fs.Path, loader: options.Loader, jsx: options.JSX.Pragma, @@ -1396,6 +1400,9 @@ pub const Bundler = struct { return null; }; input_fd = entry.fd; + if (this_parse.file_fd_ptr) |file_fd_ptr| { + file_fd_ptr.* = entry.fd; + } break :brk logger.Source.initRecycledFile(Fs.File{ .path = path, .contents = entry.contents }, bundler.allocator) catch return null; }; diff --git a/src/cli.zig b/src/cli.zig index 1418d1b2e..530dc5e30 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -192,6 +192,7 @@ pub const Arguments = struct { clap.parseParam("-u, --origin <STR> Rewrite import URLs to start with --origin. Default: \"\"") catch unreachable, clap.parseParam("-p, --port <STR> Port to serve bun's dev server on. Default: \"3000\"") catch unreachable, clap.parseParam("--hot Enable auto reload in bun's JavaScript runtime") catch unreachable, + clap.parseParam("--watch Automatically restart bun's JavaScript runtime on file change") catch unreachable, clap.parseParam("--no-install Disable auto install in bun's JavaScript runtime") catch unreachable, clap.parseParam("-i Automatically install dependencies and use global cache in bun's runtime, equivalent to --install=fallback") catch unreachable, clap.parseParam("--install <STR> Install dependencies automatically when no node_modules are present, default: \"auto\". \"force\" to ignore node_modules, fallback to install any missing") catch unreachable, @@ -440,7 +441,12 @@ pub const Arguments = struct { // we never actually supported inject. // opts.inject = args.options("--inject"); opts.extension_order = args.options("--extension-order"); - ctx.debug.hot_reload = args.flag("--hot"); + if (args.flag("--hot")) { + ctx.debug.hot_reload = .hot; + } else if (args.flag("--watch")) { + ctx.debug.hot_reload = .watch; + bun.auto_reload_on_crash = true; + } ctx.passthrough = args.remaining(); opts.no_summary = args.flag("--no-summary"); @@ -864,7 +870,7 @@ pub const Command = struct { dump_limits: bool = false, fallback_only: bool = false, silent: bool = false, - hot_reload: bool = false, + hot_reload: HotReload = HotReload.none, global_cache: options.GlobalCache = .auto, offline_mode_setting: ?Bunfig.OfflineMode = null, run_in_bun: bool = false, @@ -878,6 +884,12 @@ pub const Command = struct { test_directory: []const u8 = "", }; + pub const HotReload = enum { + none, + hot, + watch, + }; + pub const TestOptions = struct { update_snapshots: bool = false, repeat_count: u32 = 0, diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 11f57b0ad..bdcb0ee55 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -431,6 +431,9 @@ pub const TestCommand = struct { const test_files = try scanner.results.toOwnedSlice(); if (test_files.len > 0) { + vm.hot_reload = ctx.debug.hot_reload; + if (vm.hot_reload != .none) + JSC.HotReloader.enableHotModuleReloading(vm); // vm.bundler.fs.fs.readDirectory(_dir: string, _handle: ?std.fs.Dir) runAllTests(reporter, vm, test_files, ctx.allocator); } @@ -550,6 +553,19 @@ pub const TestCommand = struct { Output.prettyError("\n", .{}); Output.flush(); + if (vm.hot_reload == .watch) { + vm.eventLoop().tickPossiblyForever(); + + while (true) { + while (vm.eventLoop().tasks.count > 0 or vm.active_tasks > 0 or vm.uws_event_loop.?.active > 0) { + vm.tick(); + vm.eventLoop().autoTickActive(); + } + + vm.eventLoop().tickPossiblyForever(); + } + } + if (reporter.summary.fail > 0) { Global.exit(1); } diff --git a/src/install/install.zig b/src/install/install.zig index 75a7e28aa..b6d61e00a 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -2748,10 +2748,8 @@ pub const PackageManager = struct { var tmpfile = FileSystem.RealFS.Tmpfile{}; var secret: [32]u8 = undefined; std.mem.writeIntNative(u64, secret[0..8], @intCast(u64, std.time.milliTimestamp())); - var state = std.rand.Xoodoo.init(secret); - const rng = state.random(); var base64_bytes: [64]u8 = undefined; - rng.bytes(&base64_bytes); + std.crypto.random.bytes(&base64_bytes); const tmpname__ = std.fmt.bufPrint(tmpname_buf[8..], "{s}", .{std.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable; tmpname_buf[tmpname__.len + 8] = 0; diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index f7e872d4b..35e018b7e 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -1380,9 +1380,8 @@ pub fn saveToDisk(this: *Lockfile, filename: stringZ) void { var tmpfile = FileSystem.RealFS.Tmpfile{}; var secret: [32]u8 = undefined; std.mem.writeIntNative(u64, secret[0..8], @intCast(u64, std.time.milliTimestamp())); - var rng = std.rand.Xoodoo.init(secret); var base64_bytes: [64]u8 = undefined; - rng.random().bytes(&base64_bytes); + std.crypto.random.bytes(&base64_bytes); const tmpname__ = std.fmt.bufPrint(tmpname_buf[8..], "{s}", .{std.fmt.fmtSliceHexLower(&base64_bytes)}) catch unreachable; tmpname_buf[tmpname__.len + 8] = 0; diff --git a/src/output.zig b/src/output.zig index 72f8e57ed..96bf1b8a7 100644 --- a/src/output.zig +++ b/src/output.zig @@ -232,6 +232,13 @@ pub fn resetTerminal() void { } } +pub fn resetTerminalAll() void { + if (enable_ansi_colors_stderr) + _ = source.error_stream.write("\x1b[H\x1b[2J") catch 0; + if (enable_ansi_colors_stdout) + _ = source.stream.write("\x1b[H\x1b[2J") catch 0; +} + /// Write buffered stdout & stderr to the terminal. /// Must be called before the process exits or the buffered output will be lost. /// Bun automatically calls this function in Global.exit(). diff --git a/src/panic_handler.zig b/src/panic_handler.zig index ff17ed7d1..4e2ca05f9 100644 --- a/src/panic_handler.zig +++ b/src/panic_handler.zig @@ -36,6 +36,15 @@ pub fn NewPanicHandler(comptime panic_func: fn ([]const u8, ?*std.builtin.StackT Output.disableBuffering(); + if (bun.auto_reload_on_crash) { + // attempt to prevent a double panic + bun.auto_reload_on_crash = false; + + Output.prettyErrorln("<d>--- Bun is auto-restarting due to crash <d>[time: <b>{d}<r><d>] ---<r>", .{@max(std.time.milliTimestamp(), 0)}); + Output.flush(); + bun.reloadProcess(bun.default_allocator, false); + } + // // We want to always inline the panic handler so it doesn't show up in the stacktrace. @call(.always_inline, panic_func, .{ msg, error_return_type, addr }); } diff --git a/src/report.zig b/src/report.zig index b3724e1d2..ee0366263 100644 --- a/src/report.zig +++ b/src/report.zig @@ -558,5 +558,14 @@ pub noinline fn globalError(err: anyerror) noreturn { std.debug.writeStackTrace(trace.*, Output.errorWriter(), default_allocator, debug_info, std.debug.detectTTYConfig(std.io.getStdErr())) catch break :print_stacktrace; } + if (bun.auto_reload_on_crash) { + // attempt to prevent a double panic + bun.auto_reload_on_crash = false; + + Output.prettyErrorln("<d>---<r> Bun is auto-restarting due to crash <d>[time: <b>{d}<r><d>] ---<r>", .{@max(std.time.milliTimestamp(), 0)}); + Output.flush(); + bun.reloadProcess(bun.default_allocator, false); + } + Global.exit(1); } |