diff --git a/lib/std/child_process.zig b/lib/std/child_process.zig index 7808dcd1e52675c816b140f788cab98cb0b71dbe..52907ee080792dec98d172319af27386ef91ee39 100644 --- a/lib/std/child_process.zig +++ b/lib/std/child_process.zig @@ -19,6 +19,8 @@ const maxInt = std.math.maxInt; const assert = std.debug.assert; pub const ChildProcess = struct { + /// Available after calling `spawn()`. It is a race condition to use this + /// value after `wait()`. pid: if (builtin.os.tag == .windows) void else i32, handle: if (builtin.os.tag == .windows) windows.HANDLE else void, thread_handle: if (builtin.os.tag == .windows) windows.HANDLE else void, @@ -123,6 +125,7 @@ pub const ChildProcess = struct { } /// On success must call `kill` or `wait`. + /// After spawning the `pid` is available. pub fn spawn(self: *ChildProcess) SpawnError!void { if (builtin.os.tag == .windows) { return self.spawnWindows(); @@ -167,6 +170,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 { if (builtin.os.tag == .windows) { return self.waitWindows(); diff --git a/src/Compilation.zig b/src/Compilation.zig index 2c64657903560d13fc99892ac47eb2c7418bf328..2aeacd81b89047fc0f7caee50a34276fa95f4df6 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -358,10 +358,10 @@ pub const AllErrors = struct { return msg.renderToStdErrInner(ttyconf, stderr, "error:", .Red, 0) catch return; } - fn renderToStdErrInner( + pub fn renderToStdErrInner( msg: Message, ttyconf: std.debug.TTY.Config, - stderr_file: std.fs.File, + stderr_file: anytype, kind: []const u8, color: std.debug.TTY.Color, indent: usize, @@ -5221,3 +5221,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 916dea042983713c353d491167d935c53cbaeb9d..6c6c6927ab9212b371ee7cb7e98321f874509136 100644 --- a/src/link.zig +++ b/src/link.zig @@ -204,6 +204,8 @@ pub const File = struct { /// of this linking operation. lock: ?Cache.Lock = null, + child_pid: ?std.os.pid_t = null, + pub const LinkBlock = union { elf: Elf.TextBlock, coff: Coff.TextBlock, @@ -330,6 +332,17 @@ pub const File = struct { .coff, .elf, .macho, .plan9 => { 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 9aedb42d38f8e62de138bc11a2e282e9c053c735..a6172a9633821efa11fddae9d09bbb06f0da31e2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -578,6 +578,7 @@ fn buildOutputType( var strip = false; var function_sections = false; var watch = false; + var listen_addr: ?std.net.Ip4Address = null; var debug_compile_errors = false; var verbose_link = std.process.hasEnvVarConstant("ZIG_VERBOSE_LINK"); var verbose_cc = std.process.hasEnvVarConstant("ZIG_VERBOSE_CC"); @@ -994,6 +995,18 @@ fn buildOutputType( } else { try log_scopes.append(gpa, args[i]); } + } else if (mem.eql(u8, arg, "--listen")) { + if (i + 1 >= args.len) fatal("expected parameter after {s}", .{arg}); + i += 1; + // example: --listen 127.0.0.1:9000 + var it = std.mem.split(u8, args[i], ":"); + 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.", .{}); @@ -2649,6 +2662,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(), + .on => .escape_codes, + .off => .no_color, + }; + for (errors.list) |full_err_msg| { + try full_err_msg.renderToStdErrInner(ttyconf, conn.stream, "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(); @@ -2901,6 +3033,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..]); + } + const child = try 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,