diff --git a/src/Command.zig b/src/Command.zig index 12068adca..264c20af7 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -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. diff --git a/src/Pty.zig b/src/Pty.zig index cf03c823b..fc6a05b14 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -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, diff --git a/src/Window.zig b/src/Window.zig index 6d7f4e969..0bbb7811b 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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);