execute the child command

This commit is contained in:
Mitchell Hashimoto
2022-04-24 14:33:25 -07:00
parent c4600d584f
commit 9cc19b0553
3 changed files with 81 additions and 3 deletions

View File

@ -49,7 +49,11 @@ stderr: ?File = null,
/// If set, this will be executed /in the child process/ after fork but
/// before exec. This is useful to setup some state in the child before the
/// exec process takes over, such as signal handlers, setsid, setuid, etc.
pre_exec: ?fn () void = null,
pre_exec: ?fn (*Command) void = null,
/// User data that is sent to the callback. Set with setData and getData
/// for a more user-friendly API.
data: ?*anyopaque = null,
/// Process ID is set after start is called.
pid: ?i32 = null,
@ -121,7 +125,7 @@ pub fn start(self: *Command, alloc: Allocator) !void {
if (self.stderr) |f| try setupFd(f.handle, os.STDERR_FILENO);
// If the user requested a pre exec callback, call it now.
if (self.pre_exec) |f| f();
if (self.pre_exec) |f| f(self);
// Finally, replace our process.
_ = std.os.execveZ(pathZ, argsZ, envp) catch null;
@ -140,6 +144,19 @@ pub fn wait(self: Command) !Exit {
return Exit.init(res.status);
}
/// Sets command->data to data.
pub fn setData(self: *Command, pointer: ?*anyopaque) void {
self.data = pointer;
}
/// Returns command->data.
pub fn getData(self: Command, comptime DT: type) ?*DT {
return if (self.data) |ptr|
@ptrCast(?*DT, @alignCast(@alignOf(DT), ptr))
else
null;
}
/// Search for "cmd" in the PATH and return the absolute path. This will
/// always allocate if there is a non-null result. The caller must free the
/// resulting value.

View File

@ -14,6 +14,7 @@ const c = switch (builtin.os.tag) {
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
@ -27,6 +28,8 @@ const winsize = extern struct {
ws_ypixel: u16,
};
pub extern "c" fn setsid() std.c.pid_t;
/// The file descriptors for the master and slave side of the pty.
master: fd_t,
slave: fd_t,
@ -73,6 +76,23 @@ pub fn setSize(self: Pty, size: winsize) !void {
return error.IoctlFailed;
}
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
pub fn childPreExec(self: Pty) !void {
// Create a new process group
if (setsid() < 0) return error.ProcessGroupFailed;
// Set controlling terminal
if (std.os.linux.ioctl(self.slave, c.TIOCSCTTY, 0) < 0)
return error.SetControllingTerminalFailed;
// Can close master/slave pair now
std.os.close(self.slave);
std.os.close(self.master);
// TODO: reset signals
}
test {
var ws: winsize = .{
.ws_row = 50,

View File

@ -14,6 +14,7 @@ const glfw = @import("glfw");
const gl = @import("opengl.zig");
const libuv = @import("libuv/main.zig");
const Pty = @import("Pty.zig");
const Command = @import("Command.zig");
const Terminal = @import("terminal/Terminal.zig");
const log = std.log.scoped(.window);
@ -30,6 +31,9 @@ grid: Grid,
/// The underlying pty for this window.
pty: Pty,
/// The command we're running for our tty.
command: Command,
/// The terminal emulator internal state. This is the abstract "terminal"
/// that manages input, grid updating, etc. and is renderer-agnostic. It
/// just stores internal state about a grid. This is connected back to
@ -104,6 +108,36 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
});
errdefer pty.deinit();
// Create our child process
const path = (try Command.expandPath(alloc, "sh")) orelse
return error.CommandNotFound;
defer alloc.free(path);
var env = std.BufMap.init(alloc);
defer env.deinit();
try env.put("TERM", "dumb");
var cmd: Command = .{
.path = path,
.args = &[_][]const u8{path},
.env = &env,
.pre_exec = (struct {
fn callback(c: *Command) void {
const p = c.getData(Pty) orelse unreachable;
p.childPreExec() catch |err|
log.err("error initializing child: {}", .{err});
}
}).callback,
.data = &pty,
};
// note: can't set these in the struct initializer because it
// sets the handle to "0". Probably a stage1 zig bug.
cmd.stdin = std.fs.File{ .handle = pty.slave };
cmd.stdout = cmd.stdin;
cmd.stderr = cmd.stdin;
try cmd.start(alloc);
log.debug("started subcommand path={s} pid={}", .{ path, cmd.pid });
// Create our terminal
var term = Terminal.init(grid.size.columns, grid.size.rows);
errdefer term.deinit(alloc);
@ -121,6 +155,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
.window = window,
.grid = grid,
.pty = pty,
.command = cmd,
.terminal = term,
.cursor_timer = timer,
};
@ -142,8 +177,14 @@ pub fn destroy(self: *Window) void {
t.deinit(alloc);
}
}).callback);
self.terminal.deinit(self.alloc);
// Deinitialize the pty. This closes the pty handles. This should
// cause a close in the our subprocess so just wait for that.
self.pty.deinit();
_ = self.command.wait() catch |err|
log.err("error waiting for command to exit: {}", .{err});
self.terminal.deinit(self.alloc);
self.grid.deinit();
self.window.destroy();
self.alloc.destroy(self);