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 /// 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 /// 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. /// 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. /// Process ID is set after start is called.
pid: ?i32 = null, 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 (self.stderr) |f| try setupFd(f.handle, os.STDERR_FILENO);
// If the user requested a pre exec callback, call it now. // 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. // Finally, replace our process.
_ = std.os.execveZ(pathZ, argsZ, envp) catch null; _ = std.os.execveZ(pathZ, argsZ, envp) catch null;
@ -140,6 +144,19 @@ pub fn wait(self: Command) !Exit {
return Exit.init(res.status); 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 /// 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 /// always allocate if there is a non-null result. The caller must free the
/// resulting value. /// resulting value.

View File

@ -14,6 +14,7 @@ const c = switch (builtin.os.tag) {
@cInclude("util.h"); // openpty() @cInclude("util.h"); // openpty()
}), }),
else => @cImport({ else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h"); @cInclude("pty.h");
}), }),
}; };
@ -27,6 +28,8 @@ const winsize = extern struct {
ws_ypixel: u16, ws_ypixel: u16,
}; };
pub extern "c" fn setsid() std.c.pid_t;
/// The file descriptors for the master and slave side of the pty. /// The file descriptors for the master and slave side of the pty.
master: fd_t, master: fd_t,
slave: fd_t, slave: fd_t,
@ -73,6 +76,23 @@ pub fn setSize(self: Pty, size: winsize) !void {
return error.IoctlFailed; 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 { test {
var ws: winsize = .{ var ws: winsize = .{
.ws_row = 50, .ws_row = 50,

View File

@ -14,6 +14,7 @@ const glfw = @import("glfw");
const gl = @import("opengl.zig"); const gl = @import("opengl.zig");
const libuv = @import("libuv/main.zig"); const libuv = @import("libuv/main.zig");
const Pty = @import("Pty.zig"); const Pty = @import("Pty.zig");
const Command = @import("Command.zig");
const Terminal = @import("terminal/Terminal.zig"); const Terminal = @import("terminal/Terminal.zig");
const log = std.log.scoped(.window); const log = std.log.scoped(.window);
@ -30,6 +31,9 @@ grid: Grid,
/// The underlying pty for this window. /// The underlying pty for this window.
pty: Pty, pty: Pty,
/// The command we're running for our tty.
command: Command,
/// The terminal emulator internal state. This is the abstract "terminal" /// The terminal emulator internal state. This is the abstract "terminal"
/// that manages input, grid updating, etc. and is renderer-agnostic. It /// that manages input, grid updating, etc. and is renderer-agnostic. It
/// just stores internal state about a grid. This is connected back to /// 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(); 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 // Create our terminal
var term = Terminal.init(grid.size.columns, grid.size.rows); var term = Terminal.init(grid.size.columns, grid.size.rows);
errdefer term.deinit(alloc); errdefer term.deinit(alloc);
@ -121,6 +155,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
.window = window, .window = window,
.grid = grid, .grid = grid,
.pty = pty, .pty = pty,
.command = cmd,
.terminal = term, .terminal = term,
.cursor_timer = timer, .cursor_timer = timer,
}; };
@ -142,8 +177,14 @@ pub fn destroy(self: *Window) void {
t.deinit(alloc); t.deinit(alloc);
} }
}).callback); }).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.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.grid.deinit();
self.window.destroy(); self.window.destroy();
self.alloc.destroy(self); self.alloc.destroy(self);