diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 3bdef3177a..f8cba85874 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -185,6 +185,7 @@ pub const ChildProcess = struct { } /// Blocks until child process terminates and then cleans up all resources. + /// TODO: set the pid to undefined in this function. pub fn wait(self: *ChildProcess) !Term { const term = if (builtin.os.tag == .windows) try self.waitWindows() diff --git a/src/Compilation.zig b/src/Compilation.zig index 63dd229ec5..83e0f0d5d9 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -5663,3 +5663,10 @@ pub fn compilerRtStrip(comp: Compilation) bool { return true; } } + +pub fn hotCodeSwap(comp: *Compilation, pid: std.os.pid_t) !void { + comp.bin_file.child_pid = pid; + try comp.makeBinFileWritable(); + try comp.update(); + try comp.makeBinFileExecutable(); +} diff --git a/src/link.zig b/src/link.zig index 1ecbeadd7f..34a16a1a1b 100644 --- a/src/link.zig +++ b/src/link.zig @@ -264,6 +264,8 @@ pub const File = struct { /// of this linking operation. lock: ?Cache.Lock = null, + child_pid: ?std.os.pid_t = null, + /// Attempts incremental linking, if the file already exists. If /// incremental linking fails, falls back to truncating the file and /// rewriting it. A malicious file is detected as incremental link failure @@ -376,6 +378,17 @@ pub const File = struct { if (build_options.only_c) unreachable; if (base.file != null) return; const emit = base.options.emit orelse return; + if (base.child_pid != null) { + // If we try to open the output file in write mode while it is running, + // it will return ETXTBSY. So instead, we copy the file, atomically rename it + // over top of the exe path, and then proceed normally. This changes the inode, + // avoiding the error. + const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{ + emit.sub_path, std.crypto.random.int(u32), + }); + try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{}); + try emit.directory.handle.rename(tmp_sub_path, emit.sub_path); + } base.file = try emit.directory.handle.createFile(emit.sub_path, .{ .truncate = false, .read = true, diff --git a/src/main.zig b/src/main.zig index 3a06d0272f..a485fdb9e2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -687,6 +687,7 @@ fn buildOutputType( var function_sections = false; var no_builtin = false; var watch = false; + var listen_addr: ?std.net.Ip4Address = null; var debug_compile_errors = false; var verbose_link = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_LINK"); var verbose_cc = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_CC"); @@ -1144,6 +1145,17 @@ fn buildOutputType( } else { try log_scopes.append(gpa, args_iter.nextOrFatal()); } + } else if (mem.eql(u8, arg, "--listen")) { + const next_arg = args_iter.nextOrFatal(); + // example: --listen 127.0.0.1:9000 + var it = std.mem.split(u8, next_arg, ":"); + const host = it.next().?; + const port_text = it.next() orelse "14735"; + const port = std.fmt.parseInt(u16, port_text, 10) catch |err| + fatal("invalid port number: '{s}': {s}", .{ port_text, @errorName(err) }); + listen_addr = std.net.Ip4Address.parse(host, port) catch |err| + fatal("invalid host: '{s}': {s}", .{ host, @errorName(err) }); + watch = true; } else if (mem.eql(u8, arg, "--debug-link-snapshot")) { if (!build_options.enable_link_snapshots) { std.log.warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{}); @@ -3353,6 +3365,125 @@ fn buildOutputType( var last_cmd: ReplCmd = .help; + if (listen_addr) |ip4_addr| { + var server = std.net.StreamServer.init(.{ + .reuse_address = true, + }); + defer server.deinit(); + + try server.listen(.{ .in = ip4_addr }); + + while (true) { + const conn = try server.accept(); + defer conn.stream.close(); + + var buf: [100]u8 = undefined; + var child_pid: ?i32 = null; + + while (true) { + try comp.makeBinFileExecutable(); + + const amt = try conn.stream.read(&buf); + const line = buf[0..amt]; + const actual_line = mem.trimRight(u8, line, "\r\n "); + + const cmd: ReplCmd = blk: { + if (mem.eql(u8, actual_line, "update")) { + break :blk .update; + } else if (mem.eql(u8, actual_line, "exit")) { + break; + } else if (mem.eql(u8, actual_line, "help")) { + break :blk .help; + } else if (mem.eql(u8, actual_line, "run")) { + break :blk .run; + } else if (mem.eql(u8, actual_line, "update-and-run")) { + break :blk .update_and_run; + } else if (actual_line.len == 0) { + break :blk last_cmd; + } else { + try stderr.print("unknown command: {s}\n", .{actual_line}); + continue; + } + }; + last_cmd = cmd; + switch (cmd) { + .update => { + tracy.frameMark(); + if (output_mode == .Exe) { + try comp.makeBinFileWritable(); + } + updateModule(gpa, comp, hook) catch |err| switch (err) { + error.SemanticAnalyzeFail => continue, + else => |e| return e, + }; + }, + .help => { + try stderr.writeAll(repl_help); + }, + .run => { + tracy.frameMark(); + try runOrTest( + comp, + gpa, + arena, + test_exec_args.items, + self_exe_path.?, + arg_mode, + target_info, + watch, + &comp_destroyed, + all_args, + runtime_args_start, + link_libc, + ); + }, + .update_and_run => { + tracy.frameMark(); + if (child_pid) |pid| { + try conn.stream.writer().print("hot code swap requested for pid {d}", .{pid}); + try comp.hotCodeSwap(pid); + + var errors = try comp.getAllErrorsAlloc(); + defer errors.deinit(comp.gpa); + + if (errors.list.len != 0) { + const ttyconf: std.debug.TTY.Config = switch (comp.color) { + .auto => std.debug.detectTTYConfig(std.io.getStdErr()), + .on => .escape_codes, + .off => .no_color, + }; + for (errors.list) |full_err_msg| { + try full_err_msg.renderToWriter(ttyconf, conn.stream.writer(), "error:", .Red, 0); + } + continue; + } + } else { + if (output_mode == .Exe) { + try comp.makeBinFileWritable(); + } + updateModule(gpa, comp, hook) catch |err| switch (err) { + error.SemanticAnalyzeFail => continue, + else => |e| return e, + }; + try comp.makeBinFileExecutable(); + + child_pid = try runOrTestHotSwap( + comp, + gpa, + arena, + test_exec_args.items, + self_exe_path.?, + arg_mode, + all_args, + runtime_args_start, + ); + } + }, + } + } + } + } + while (watch) { try stderr.print("(zig) ", .{}); try comp.makeBinFileExecutable(); @@ -3631,6 +3762,62 @@ fn runOrTest( } } +fn runOrTestHotSwap( + comp: *Compilation, + gpa: Allocator, + arena: Allocator, + test_exec_args: []const ?[]const u8, + self_exe_path: []const u8, + arg_mode: ArgMode, + all_args: []const []const u8, + runtime_args_start: ?usize, +) !i32 { + const exe_emit = comp.bin_file.options.emit.?; + // A naive `directory.join` here will indeed get the correct path to the binary, + // however, in the case of cwd, we actually want `./foo` so that the path can be executed. + const exe_path = try fs.path.join(arena, &[_][]const u8{ + exe_emit.directory.path orelse ".", exe_emit.sub_path, + }); + + var argv = std.ArrayList([]const u8).init(gpa); + defer argv.deinit(); + + if (test_exec_args.len == 0) { + // when testing pass the zig_exe_path to argv + if (arg_mode == .zig_test) + try argv.appendSlice(&[_][]const u8{ + exe_path, self_exe_path, + }) + // when running just pass the current exe + else + try argv.appendSlice(&[_][]const u8{ + exe_path, + }); + } else { + for (test_exec_args) |arg| { + if (arg) |a| { + try argv.append(a); + } else { + try argv.appendSlice(&[_][]const u8{ + exe_path, self_exe_path, + }); + } + } + } + if (runtime_args_start) |i| { + try argv.appendSlice(all_args[i..]); + } + var child = std.ChildProcess.init(argv.items, arena); + + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + try child.spawn(); + + return child.pid; +} + const AfterUpdateHook = union(enum) { none, print_emit_bin_dir_path,