authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-01-14 17:04:03-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-03-15 10:48:12-07:00
logae8e7c8f5a6065d12087aa547971001a399667b9
treecc59669cad88af760c12219e4bd0776240502bcc
parentee693bfe04522efe556cb416050326187f168aea

stage2: hot code swapping PoC

* CLI supports --listen to accept commands on a socket * make it able to produce an updated executable while it is running

4 files changed, 208 insertions(+), 0 deletions(-)

lib/std/child_process.zig+1
...@@ -185,6 +185,7 @@ pub const ChildProcess = struct {...@@ -185,6 +185,7 @@ pub const ChildProcess = struct {
185 }185 }
186186
187 /// Blocks until child process terminates and then cleans up all resources.187 /// Blocks until child process terminates and then cleans up all resources.
188 /// TODO: set the pid to undefined in this function.
188 pub fn wait(self: *ChildProcess) !Term {189 pub fn wait(self: *ChildProcess) !Term {
189 const term = if (builtin.os.tag == .windows)190 const term = if (builtin.os.tag == .windows)
190 try self.waitWindows()191 try self.waitWindows()
src/Compilation.zig+7
...@@ -5663,3 +5663,10 @@ pub fn compilerRtStrip(comp: Compilation) bool {...@@ -5663,3 +5663,10 @@ pub fn compilerRtStrip(comp: Compilation) bool {
5663 return true;5663 return true;
5664 }5664 }
5665}5665}
5666
5667pub fn hotCodeSwap(comp: *Compilation, pid: std.os.pid_t) !void {
5668 comp.bin_file.child_pid = pid;
5669 try comp.makeBinFileWritable();
5670 try comp.update();
5671 try comp.makeBinFileExecutable();
5672}
src/link.zig+13
...@@ -264,6 +264,8 @@ pub const File = struct {...@@ -264,6 +264,8 @@ pub const File = struct {
264 /// of this linking operation.264 /// of this linking operation.
265 lock: ?Cache.Lock = null,265 lock: ?Cache.Lock = null,
266266
267 child_pid: ?std.os.pid_t = null,
268
267 /// Attempts incremental linking, if the file already exists. If269 /// Attempts incremental linking, if the file already exists. If
268 /// incremental linking fails, falls back to truncating the file and270 /// incremental linking fails, falls back to truncating the file and
269 /// rewriting it. A malicious file is detected as incremental link failure271 /// rewriting it. A malicious file is detected as incremental link failure
...@@ -376,6 +378,17 @@ pub const File = struct {...@@ -376,6 +378,17 @@ pub const File = struct {
376 if (build_options.only_c) unreachable;378 if (build_options.only_c) unreachable;
377 if (base.file != null) return;379 if (base.file != null) return;
378 const emit = base.options.emit orelse return;380 const emit = base.options.emit orelse return;
381 if (base.child_pid != null) {
382 // If we try to open the output file in write mode while it is running,
383 // it will return ETXTBSY. So instead, we copy the file, atomically rename it
384 // over top of the exe path, and then proceed normally. This changes the inode,
385 // avoiding the error.
386 const tmp_sub_path = try std.fmt.allocPrint(base.allocator, "{s}-{x}", .{
387 emit.sub_path, std.crypto.random.int(u32),
388 });
389 try emit.directory.handle.copyFile(emit.sub_path, emit.directory.handle, tmp_sub_path, .{});
390 try emit.directory.handle.rename(tmp_sub_path, emit.sub_path);
391 }
379 base.file = try emit.directory.handle.createFile(emit.sub_path, .{392 base.file = try emit.directory.handle.createFile(emit.sub_path, .{
380 .truncate = false,393 .truncate = false,
381 .read = true,394 .read = true,
src/main.zig+187
...@@ -687,6 +687,7 @@ fn buildOutputType(...@@ -687,6 +687,7 @@ fn buildOutputType(
687 var function_sections = false;687 var function_sections = false;
688 var no_builtin = false;688 var no_builtin = false;
689 var watch = false;689 var watch = false;
690 var listen_addr: ?std.net.Ip4Address = null;
690 var debug_compile_errors = false;691 var debug_compile_errors = false;
691 var verbose_link = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_LINK");692 var verbose_link = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_LINK");
692 var verbose_cc = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_CC");693 var verbose_cc = (builtin.os.tag != .wasi or builtin.link_libc) and std.process.hasEnvVarConstant("ZIG_VERBOSE_CC");
...@@ -1144,6 +1145,17 @@ fn buildOutputType(...@@ -1144,6 +1145,17 @@ fn buildOutputType(
1144 } else {1145 } else {
1145 try log_scopes.append(gpa, args_iter.nextOrFatal());1146 try log_scopes.append(gpa, args_iter.nextOrFatal());
1146 }1147 }
1148 } else if (mem.eql(u8, arg, "--listen")) {
1149 const next_arg = args_iter.nextOrFatal();
1150 // example: --listen 127.0.0.1:9000
1151 var it = std.mem.split(u8, next_arg, ":");
1152 const host = it.next().?;
1153 const port_text = it.next() orelse "14735";
1154 const port = std.fmt.parseInt(u16, port_text, 10) catch |err|
1155 fatal("invalid port number: '{s}': {s}", .{ port_text, @errorName(err) });
1156 listen_addr = std.net.Ip4Address.parse(host, port) catch |err|
1157 fatal("invalid host: '{s}': {s}", .{ host, @errorName(err) });
1158 watch = true;
1147 } else if (mem.eql(u8, arg, "--debug-link-snapshot")) {1159 } else if (mem.eql(u8, arg, "--debug-link-snapshot")) {
1148 if (!build_options.enable_link_snapshots) {1160 if (!build_options.enable_link_snapshots) {
1149 std.log.warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{});1161 std.log.warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{});
...@@ -3353,6 +3365,125 @@ fn buildOutputType(...@@ -3353,6 +3365,125 @@ fn buildOutputType(
33533365
3354 var last_cmd: ReplCmd = .help;3366 var last_cmd: ReplCmd = .help;
33553367
3368 if (listen_addr) |ip4_addr| {
3369 var server = std.net.StreamServer.init(.{
3370 .reuse_address = true,
3371 });
3372 defer server.deinit();
3373
3374 try server.listen(.{ .in = ip4_addr });
3375
3376 while (true) {
3377 const conn = try server.accept();
3378 defer conn.stream.close();
3379
3380 var buf: [100]u8 = undefined;
3381 var child_pid: ?i32 = null;
3382
3383 while (true) {
3384 try comp.makeBinFileExecutable();
3385
3386 const amt = try conn.stream.read(&buf);
3387 const line = buf[0..amt];
3388 const actual_line = mem.trimRight(u8, line, "\r\n ");
3389
3390 const cmd: ReplCmd = blk: {
3391 if (mem.eql(u8, actual_line, "update")) {
3392 break :blk .update;
3393 } else if (mem.eql(u8, actual_line, "exit")) {
3394 break;
3395 } else if (mem.eql(u8, actual_line, "help")) {
3396 break :blk .help;
3397 } else if (mem.eql(u8, actual_line, "run")) {
3398 break :blk .run;
3399 } else if (mem.eql(u8, actual_line, "update-and-run")) {
3400 break :blk .update_and_run;
3401 } else if (actual_line.len == 0) {
3402 break :blk last_cmd;
3403 } else {
3404 try stderr.print("unknown command: {s}\n", .{actual_line});
3405 continue;
3406 }
3407 };
3408 last_cmd = cmd;
3409 switch (cmd) {
3410 .update => {
3411 tracy.frameMark();
3412 if (output_mode == .Exe) {
3413 try comp.makeBinFileWritable();
3414 }
3415 updateModule(gpa, comp, hook) catch |err| switch (err) {
3416 error.SemanticAnalyzeFail => continue,
3417 else => |e| return e,
3418 };
3419 },
3420 .help => {
3421 try stderr.writeAll(repl_help);
3422 },
3423 .run => {
3424 tracy.frameMark();
3425 try runOrTest(
3426 comp,
3427 gpa,
3428 arena,
3429 test_exec_args.items,
3430 self_exe_path.?,
3431 arg_mode,
3432 target_info,
3433 watch,
3434 &comp_destroyed,
3435 all_args,
3436 runtime_args_start,
3437 link_libc,
3438 );
3439 },
3440 .update_and_run => {
3441 tracy.frameMark();
3442 if (child_pid) |pid| {
3443 try conn.stream.writer().print("hot code swap requested for pid {d}", .{pid});
3444 try comp.hotCodeSwap(pid);
3445
3446 var errors = try comp.getAllErrorsAlloc();
3447 defer errors.deinit(comp.gpa);
3448
3449 if (errors.list.len != 0) {
3450 const ttyconf: std.debug.TTY.Config = switch (comp.color) {
3451 .auto => std.debug.detectTTYConfig(std.io.getStdErr()),
3452 .on => .escape_codes,
3453 .off => .no_color,
3454 };
3455 for (errors.list) |full_err_msg| {
3456 try full_err_msg.renderToWriter(ttyconf, conn.stream.writer(), "error:", .Red, 0);
3457 }
3458 continue;
3459 }
3460 } else {
3461 if (output_mode == .Exe) {
3462 try comp.makeBinFileWritable();
3463 }
3464 updateModule(gpa, comp, hook) catch |err| switch (err) {
3465 error.SemanticAnalyzeFail => continue,
3466 else => |e| return e,
3467 };
3468 try comp.makeBinFileExecutable();
3469
3470 child_pid = try runOrTestHotSwap(
3471 comp,
3472 gpa,
3473 arena,
3474 test_exec_args.items,
3475 self_exe_path.?,
3476 arg_mode,
3477 all_args,
3478 runtime_args_start,
3479 );
3480 }
3481 },
3482 }
3483 }
3484 }
3485 }
3486
3356 while (watch) {3487 while (watch) {
3357 try stderr.print("(zig) ", .{});3488 try stderr.print("(zig) ", .{});
3358 try comp.makeBinFileExecutable();3489 try comp.makeBinFileExecutable();
...@@ -3631,6 +3762,62 @@ fn runOrTest(...@@ -3631,6 +3762,62 @@ fn runOrTest(
3631 }3762 }
3632}3763}
36333764
3765fn runOrTestHotSwap(
3766 comp: *Compilation,
3767 gpa: Allocator,
3768 arena: Allocator,
3769 test_exec_args: []const ?[]const u8,
3770 self_exe_path: []const u8,
3771 arg_mode: ArgMode,
3772 all_args: []const []const u8,
3773 runtime_args_start: ?usize,
3774) !i32 {
3775 const exe_emit = comp.bin_file.options.emit.?;
3776 // A naive `directory.join` here will indeed get the correct path to the binary,
3777 // however, in the case of cwd, we actually want `./foo` so that the path can be executed.
3778 const exe_path = try fs.path.join(arena, &[_][]const u8{
3779 exe_emit.directory.path orelse ".", exe_emit.sub_path,
3780 });
3781
3782 var argv = std.ArrayList([]const u8).init(gpa);
3783 defer argv.deinit();
3784
3785 if (test_exec_args.len == 0) {
3786 // when testing pass the zig_exe_path to argv
3787 if (arg_mode == .zig_test)
3788 try argv.appendSlice(&[_][]const u8{
3789 exe_path, self_exe_path,
3790 })
3791 // when running just pass the current exe
3792 else
3793 try argv.appendSlice(&[_][]const u8{
3794 exe_path,
3795 });
3796 } else {
3797 for (test_exec_args) |arg| {
3798 if (arg) |a| {
3799 try argv.append(a);
3800 } else {
3801 try argv.appendSlice(&[_][]const u8{
3802 exe_path, self_exe_path,
3803 });
3804 }
3805 }
3806 }
3807 if (runtime_args_start) |i| {
3808 try argv.appendSlice(all_args[i..]);
3809 }
3810 var child = std.ChildProcess.init(argv.items, arena);
3811
3812 child.stdin_behavior = .Inherit;
3813 child.stdout_behavior = .Inherit;
3814 child.stderr_behavior = .Inherit;
3815
3816 try child.spawn();
3817
3818 return child.pid;
3819}
3820
3634const AfterUpdateHook = union(enum) {3821const AfterUpdateHook = union(enum) {
3635 none,3822 none,
3636 print_emit_bin_dir_path,3823 print_emit_bin_dir_path,