const std = @import("std"); const assert = std.debug.assert; const cli = @import("cli.zig"); const fs = @import("fs.zig"); const conf = @import("conf.zig"); const time = @import("time.zig"); const Env = @import("env.zig"); const fatal = cli.fatal; pub const Exec_Args = struct { env: *const Env, argv: []const []const u8, cwd: ?*const fs.Path = null, }; pub const Exec_Result = union(enum) { success: ?[]const u8, failure: ?[]const u8, }; /// Execute a binary and return its result. Both the success and the failure /// messages are the stdout and stderr output of the binary and the caller is /// responsible to deallocate this output. pub fn exec(args: Exec_Args) Exec_Result { return spawn_proc(args, true); } /// Run a binary and forward all its output to stdout/stderr directly. It /// returns if the execution was successful or not. pub fn run(args: Exec_Args) bool { const res = spawn_proc(args, false); switch (res) { .success => |msg| { assert(msg == null); return true; }, .failure => |msg| { assert(msg == null); return false; }, } } pub fn edit_config_file(env: *const Env, file: *const fs.Path, comptime verify: fn (env: *const Env, vars: conf.Config) bool) !void { const tmp_file = tmp_edit_file(env, file); fs.cp_file(file, &tmp_file) catch |err| { switch (err) { error.FileNotFound => try fs.touch(&tmp_file), else => return err, } }; edit: while (true) { const vars: ?conf.Config = parse: while (true) { try exec_editor(env, &tmp_file); const parsed = try conf.Config.from_file(env.allocator, file); switch (parsed) { .conf => |c| break :parse c, .err => |e| { env.err("{s} (line {d})", .{ e.msg, e.line }); switch (ask_edit_again()) { .again => continue, .cancel => break :parse null, } }, } }; if (vars) |v| { defer v.deinit(); if (verify(env, v)) { try fs.mv_file(&tmp_file, file); return; } switch (ask_edit_again()) { .again => continue, .cancel => break :edit, } } else { break :edit; } } try fs.rm(&tmp_file); } pub fn edit_config_file_noverify(env: *const Env, file: *const fs.Path) !void { try edit_config_file(env, file, struct { fn noverify(_: *const Env, _: conf.Config) bool { return true; } }.noverify); } fn ask_edit_again() enum { again, cancel } { while (true) { var again_buf: [16]u8 = undefined; const again = cli.ask("Edit again? (yes/no) [yes] ", .{}, &again_buf) orelse "yes"; for (again, 0..) |c, i| again_buf[i] = std.ascii.toLower(c); if (std.mem.eql(u8, again, "y") or std.mem.eql(u8, again, "yes")) { return .again; } if (std.mem.eql(u8, again, "n") or std.mem.eql(u8, again, "no")) { cli.stderr.print_line("cancelled (changes discarded)", .{}); cli.stderr.flush(); return .cancel; } } } fn exec_editor(env: *const Env, file: *const fs.Path) !void { var configured_editor_buf: [fs.max_path_bytes]u8 = undefined; var configured_editor: ?[:0]u8 = null; if (env.editor_cmd) |cmd| { @memcpy(configured_editor_buf[0..cmd.len], cmd); configured_editor_buf[cmd.len] = 0; configured_editor = configured_editor_buf[0..cmd.len :0]; } const editor = configured_editor orelse env.get("VISUAL") orelse env.get("EDITOR") orelse "vi"; const args: Exec_Args = .{ .env = env, .argv = &.{ editor, file.name() }, }; const res = spawn_proc(args, false); switch (res) { .success => |msg| { assert(msg == null); }, .failure => |msg| { assert(msg == null); fatal("failed to open editor", .{}); }, } } /// Derive the filename for the temporary file of `file`. /// The filename is "{relname}.{timestamp}", where `relname` is the relative /// part of file regarding to etc. fn tmp_edit_file(env: *const Env, file: *const fs.Path) fs.Path { const etc = env.etc_path(.{}); const relname = file.relname(&etc).?; var tmp_filename_buf: [std.Io.Dir.max_name_bytes]u8 = undefined; var tmp_filename_len: usize = 0; tmp_filename_len += std.fmt.printInt(tmp_filename_buf[tmp_filename_len..], time.now(), 16, .lower, .{}); tmp_filename_buf[tmp_filename_len] = '.'; tmp_filename_len += 1; for (relname) |c| { tmp_filename_buf[tmp_filename_len] = if (c == std.fs.path.sep) '_' else c; tmp_filename_len += 1; } return env.work_path(.{ "edit", tmp_filename_buf[0..tmp_filename_len] }); } fn spawn_proc(args: Exec_Args, need_stdout: bool) Exec_Result { assert(args.argv.len > 0); var stdout_pipe: [2]std.posix.fd_t = undefined; if (need_stdout) { if (std.posix.system.pipe(&stdout_pipe) != 0) { fatal("error executing {s}: unable to create pipe", .{args.argv[0]}); } } const pid = std.posix.system.fork(); switch (pid) { -1 => { if (need_stdout) { _ = std.posix.system.close(stdout_pipe[0]); _ = std.posix.system.close(stdout_pipe[1]); } fatal("error executing {s}: unable to fork", .{args.argv[0]}); }, 0 => { // child process if (need_stdout) { if (std.posix.system.dup2(stdout_pipe[1], std.posix.STDOUT_FILENO) == -1) unreachable; if (std.posix.system.dup2(stdout_pipe[1], std.posix.STDERR_FILENO) == -1) unreachable; _ = std.posix.system.close(stdout_pipe[0]); _ = std.posix.system.close(stdout_pipe[1]); } const argv = args.env.allocator.allocSentinel(?[*:0]const u8, args.argv.len, null) catch { fatal("error executing {s}: out of memory", .{args.argv[0]}); }; for (args.argv, 0..) |arg, i| { argv[i] = args.env.allocator.dupeZ(u8, arg) catch { fatal("error executing {s}: out of memory", .{args.argv[0]}); }; } var env: [512:null]?[*:0]const u8 = undefined; var envlen: usize = 0; while (std.posix.system.environ[envlen]) |e| { env[envlen] = e; envlen += 1; if (envlen >= env.len - 1) break; } env[envlen] = null; if (args.cwd) |cwd| { if (std.posix.system.chdir(cwd.name()) == -1) { fatal("error executing {s}: chdir failed", .{args.argv[0]}); } } const sent = std.mem.findSentinel(u8, 0, argv[0].?); const name = argv[0].?[0..sent :0]; var eaccess = false; if (std.mem.findScalar(u8, name, '/')) |_| { // We have a relative or absolute path. No need to consult the PATH env. const res = std.posix.system.execve(name, argv, &env); switch (std.posix.errno(res)) { .NOENT => {}, .ACCES => eaccess = true, else => |err| fatal("error executing {s}: {}", .{ name, err }), } } else { var path_env = args.env.get("PATH") orelse ""; var path: fs.Path = undefined; while (path_env.len > 0) { const colon = std.mem.findScalar(u8, path_env, ':') orelse path_env.len; path = .join(.{ if (colon == 0) "." else path_env[0..colon], name }); path_env = if (colon == path_env.len) "" else path_env[colon + 1 ..]; const res = std.posix.system.execve(path.name(), argv, &env); switch (std.posix.errno(res)) { .NOENT => {}, .ACCES => eaccess = true, else => |err| fatal("error executing {s}: {}", .{ name, err }), } } } if (eaccess) { fatal("permission denied: {s}", .{name}); } else { fatal("not found: {s}", .{name}); } }, else => { // parent process var stdout_buf: []u8 = &[0]u8{}; var stdout_len: usize = 0; defer if (stdout_buf.len > 0) args.env.dealloc(stdout_buf); if (need_stdout) { _ = std.posix.system.close(stdout_pipe[1]); defer _ = std.posix.system.close(stdout_pipe[0]); while (true) { if (stdout_len >= stdout_buf.len) { const newbuf = args.env.alloc(stdout_buf.len + 1024); @memcpy(newbuf[0..stdout_len], stdout_buf[0..stdout_len]); args.env.dealloc(stdout_buf); stdout_buf = newbuf; } const res = std.posix.system.read(stdout_pipe[0], stdout_buf.ptr + stdout_len, stdout_buf.len - stdout_len); if (res < 0) { if (std.posix.errno(res) == .INTR) continue; fatal("error executing {s}: cannot read stdout", .{args.argv[0]}); } if (res == 0) break; const n: usize = @intCast(res); stdout_len += n; } } var status: c_int = undefined; while (true) { const res = std.posix.system.waitpid(pid, &status, 0); if (res < 0) { if (std.posix.errno(res) == std.posix.E.INTR) continue; fatal("error executing {s}: failed waiting for process", .{args.argv[0]}); } const s: u32 = @intCast(status); if (std.posix.W.IFEXITED(s)) { const code = std.posix.W.EXITSTATUS(s); const output: ?[]const u8 = if (stdout_len > 0) args.env.dupe(stdout_buf[0..stdout_len]) else null; return if (code == 0) .{ .success = output } else .{ .failure = output }; } else if (std.posix.W.IFSIGNALED(s)) { fatal("signal received executing {s}", .{args.argv[0]}); } else if (std.posix.W.IFSTOPPED(s)) { unreachable; } else { fatal("unknown error executing {s}", .{args.argv[0]}); } } }, } } fn without_trailing_newline(s: []const u8) []const u8 { return if (s.len > 0 and s[s.len - 1] == '\n') s[0 .. s.len - 1] else s; }