mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
core: add function to get process info from the surface (#11639)
This adds a function to the core surface to get process information about the process(es) running in the terminal. Currently supported is the PID of the foreground process and the name of the slave PTY. If there is an error retrieving the information, or the platform does not support retieving that information `null` is returned. This will be useful in exposing the foreground PID and slave PTY name to AppleScript or other APIs.
This commit is contained in:
@@ -36,6 +36,7 @@ const App = @import("App.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const inspectorpkg = @import("inspector/main.zig");
|
||||
const SurfaceMouse = @import("surface_mouse.zig");
|
||||
const ProcessInfo = @import("pty.zig").ProcessInfo;
|
||||
|
||||
const log = std.log.scoped(.surface);
|
||||
|
||||
@@ -6342,6 +6343,13 @@ fn testMouseSelectionIsNull(
|
||||
);
|
||||
}
|
||||
|
||||
/// Get information about the process(es) running within the surface. Returns
|
||||
/// `null` if there was an error getting the information or the information is
|
||||
/// not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return self.io.getProcessInfo(info);
|
||||
}
|
||||
|
||||
test "Surface: selection logic" {
|
||||
// We disable format to make these easier to
|
||||
// read by pairing sets of coordinates per line.
|
||||
|
||||
@@ -135,6 +135,33 @@ pub fn add(
|
||||
// Every exe needs the terminal options
|
||||
self.config.terminalOptions().add(b, step.root_module);
|
||||
|
||||
// C imports needed to manage/create PTYs
|
||||
switch (target.result.os.tag) {
|
||||
.freebsd,
|
||||
.linux,
|
||||
.macos,
|
||||
=> {
|
||||
const c = b.addTranslateC(.{
|
||||
.root_source_file = b.path("src/pty.c"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
switch (target.result.os.tag) {
|
||||
.macos => {
|
||||
const libc = try std.zig.LibCInstallation.findNative(.{
|
||||
.allocator = b.allocator,
|
||||
.target = &target.result,
|
||||
.verbose = false,
|
||||
});
|
||||
c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
step.root_module.addImport("pty-c", c.createModule());
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Freetype. We always include this even if our font backend doesn't
|
||||
// use it because Dear Imgui uses Freetype.
|
||||
_ = b.systemIntegrationOption("freetype", .{}); // Shows it in help
|
||||
|
||||
40
src/pty.c
Normal file
40
src/pty.c
Normal file
@@ -0,0 +1,40 @@
|
||||
#if defined(__FreeBSD__)
|
||||
|
||||
#include <termios.h> // ioctl and constants
|
||||
#include <libutil.h> // openpty
|
||||
#include <stdlib.h> // ptsname_r
|
||||
#include <unistd.h> // tcgetpgrp
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
#define _GNU_SOURCE // ptsname_r
|
||||
#include <pty.h> // openpty
|
||||
#include <stdlib.h> // ptsname_r
|
||||
#include <sys/ioctl.h> // ioctl and constants
|
||||
#include <unistd.h> // tcgetpgrp, setsid
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
#include <sys/ioctl.h> // ioctl and constants
|
||||
#include <sys/ttycom.h> // ioctl and constants for TIOCPTYGNAME
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h> // tcgetpgrp
|
||||
#include <util.h> // openpty
|
||||
|
||||
#ifndef tiocsctty
|
||||
#define tiocsctty 536900705
|
||||
#endif
|
||||
|
||||
#ifndef tiocswinsz
|
||||
#define tiocswinsz 2148037735
|
||||
#endif
|
||||
|
||||
#ifndef tiocgwinsz
|
||||
#define tiocgwinsz 1074295912
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#error "unsupported platform"
|
||||
|
||||
#endif
|
||||
144
src/pty.zig
144
src/pty.zig
@@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const windows = @import("os/main.zig").windows;
|
||||
const posix = std.posix;
|
||||
const assert = @import("quirks.zig").inlineAssert;
|
||||
|
||||
const log = std.log.scoped(.pty);
|
||||
|
||||
@@ -35,6 +36,21 @@ pub const Mode = packed struct {
|
||||
echo: bool = true,
|
||||
};
|
||||
|
||||
pub const ProcessInfo = enum {
|
||||
/// The PID of the process that controls the PTY.
|
||||
foreground_pid,
|
||||
/// Gets the name of the slave PTY. Returned name points to an internal buffer
|
||||
/// so it should not be modified or freed.
|
||||
tty_name,
|
||||
|
||||
pub fn Type(comptime info: ProcessInfo) type {
|
||||
return switch (info) {
|
||||
.foreground_pid => u64,
|
||||
.tty_name => [:0]const u8,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// A pty implementation that does nothing.
|
||||
//
|
||||
// TODO: This should be removed. This is only temporary until we have
|
||||
@@ -78,36 +94,24 @@ const NullPty = struct {
|
||||
pub fn childPreExec(self: Pty) ChildPreExecError!void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Get information about the process(es) attached to the PTY. Returns
|
||||
/// `null` if there was an error getting the information or the information
|
||||
/// is not available on a particular platform.
|
||||
pub fn getProcessInfo(_: *Pty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Linux PTY creation and management. This is just a thin layer on top
|
||||
/// of Linux syscalls. The caller is responsible for detail-oriented handling
|
||||
/// Posix PTY creation and management. This is just a thin layer on top
|
||||
/// of Posix syscalls. The caller is responsible for detail-oriented handling
|
||||
/// of the returned file handles.
|
||||
const PosixPty = struct {
|
||||
pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError;
|
||||
|
||||
pub const Fd = posix.fd_t;
|
||||
|
||||
// https://github.com/ziglang/zig/issues/13277
|
||||
// Once above is fixed, use `c.TIOCSCTTY`
|
||||
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
|
||||
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
|
||||
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
|
||||
extern "c" fn setsid() std.c.pid_t;
|
||||
const c = switch (builtin.os.tag) {
|
||||
.macos => @cImport({
|
||||
@cInclude("sys/ioctl.h"); // ioctl and constants
|
||||
@cInclude("util.h"); // openpty()
|
||||
}),
|
||||
.freebsd => @cImport({
|
||||
@cInclude("termios.h"); // ioctl and constants
|
||||
@cInclude("libutil.h"); // openpty()
|
||||
}),
|
||||
else => @cImport({
|
||||
@cInclude("sys/ioctl.h"); // ioctl and constants
|
||||
@cInclude("pty.h");
|
||||
}),
|
||||
};
|
||||
const c = @import("pty-c");
|
||||
|
||||
/// The file descriptors for the master and slave side of the pty.
|
||||
/// The slave side is never closed automatically by this struct
|
||||
@@ -116,6 +120,14 @@ const PosixPty = struct {
|
||||
master: Fd,
|
||||
slave: Fd,
|
||||
|
||||
/// Buffer for storage of slave tty name so that we don't have to recompute
|
||||
/// it every time we need it.
|
||||
tty_name_buf: [std.fs.max_path_bytes:0]u8 = undefined,
|
||||
/// The name of slave tty. If `null` it has not yet been computed or
|
||||
/// may not be available. Should not be accessed directly, but through
|
||||
/// `self.getProcessInfo(.tty_name)`
|
||||
tty_name: ?[:0]const u8 = null,
|
||||
|
||||
pub const OpenError = error{OpenptyFailed};
|
||||
|
||||
/// Open a new PTY with the given initial size.
|
||||
@@ -141,15 +153,15 @@ const PosixPty = struct {
|
||||
// Set CLOEXEC on the master fd, only the slave fd should be inherited
|
||||
// by the child process (shell/command).
|
||||
cloexec: {
|
||||
const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| {
|
||||
const flags = posix.fcntl(master_fd, posix.F.GETFD, 0) catch |err| {
|
||||
log.warn("error getting flags for master fd err={}", .{err});
|
||||
break :cloexec;
|
||||
};
|
||||
|
||||
_ = std.posix.fcntl(
|
||||
_ = posix.fcntl(
|
||||
master_fd,
|
||||
std.posix.F.SETFD,
|
||||
flags | std.posix.FD_CLOEXEC,
|
||||
posix.F.SETFD,
|
||||
flags | posix.FD_CLOEXEC,
|
||||
) catch |err| {
|
||||
log.warn("error setting CLOEXEC on master fd err={}", .{err});
|
||||
break :cloexec;
|
||||
@@ -168,6 +180,8 @@ const PosixPty = struct {
|
||||
return .{
|
||||
.master = master_fd,
|
||||
.slave = slave_fd,
|
||||
.tty_name_buf = undefined,
|
||||
.tty_name = null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,7 +208,7 @@ const PosixPty = struct {
|
||||
/// Return the size of the pty.
|
||||
pub fn getSize(self: Pty) GetSizeError!winsize {
|
||||
var ws: winsize = undefined;
|
||||
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0)
|
||||
if (c.ioctl(self.master, c.TIOCGWINSZ, @intFromPtr(&ws)) < 0)
|
||||
return error.IoctlFailed;
|
||||
|
||||
return ws;
|
||||
@@ -204,7 +218,7 @@ const PosixPty = struct {
|
||||
|
||||
/// Set the size of the pty.
|
||||
pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
|
||||
if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0)
|
||||
if (c.ioctl(self.master, c.TIOCSWINSZ, @intFromPtr(&size)) < 0)
|
||||
return error.IoctlFailed;
|
||||
}
|
||||
|
||||
@@ -234,10 +248,10 @@ const PosixPty = struct {
|
||||
posix.sigaction(posix.SIG.QUIT, &sa, null);
|
||||
|
||||
// Create a new process group
|
||||
if (setsid() < 0) return error.ProcessGroupFailed;
|
||||
if (c.setsid() < 0) return error.ProcessGroupFailed;
|
||||
|
||||
// Set controlling terminal
|
||||
switch (posix.errno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) {
|
||||
switch (posix.errno(c.ioctl(self.slave, c.TIOCSCTTY, @as(c_ulong, 0)))) {
|
||||
.SUCCESS => {},
|
||||
else => |err| {
|
||||
log.err("error setting controlling terminal errno={}", .{err});
|
||||
@@ -249,6 +263,62 @@ const PosixPty = struct {
|
||||
posix.close(self.slave);
|
||||
posix.close(self.master);
|
||||
}
|
||||
|
||||
/// Get information about the process(es) attached to the PTY. Returns
|
||||
/// `null` if there was an error getting the information or the information
|
||||
/// is not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *PosixPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return switch (info) {
|
||||
.foreground_pid => {
|
||||
switch (builtin.os.tag) {
|
||||
.linux => {
|
||||
const linux = std.os.linux;
|
||||
var pgrp: i32 = undefined;
|
||||
const rc = linux.tcgetpgrp(self.master, &pgrp);
|
||||
switch (linux.E.init(rc)) {
|
||||
.SUCCESS => return @intCast(pgrp),
|
||||
else => return null,
|
||||
}
|
||||
},
|
||||
else => {
|
||||
const rc = c.tcgetpgrp(self.master);
|
||||
if (rc < 0) return null;
|
||||
return @intCast(rc);
|
||||
},
|
||||
}
|
||||
},
|
||||
.tty_name => {
|
||||
if (self.tty_name) |tty_name| return tty_name;
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.macos => {
|
||||
// The macOS TIOCPTYGNAME ioctl does not allow us to
|
||||
// specify the length of the buffer passed to it, but
|
||||
// expects it to be at least 128 bytes long.
|
||||
assert(self.tty_name_buf.len >= 128);
|
||||
switch (posix.errno(c.ioctl(self.master, c.TIOCPTYGNAME, @intFromPtr(&self.tty_name_buf)))) {
|
||||
.SUCCESS => {
|
||||
const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
|
||||
self.tty_name = tty_name;
|
||||
return tty_name;
|
||||
},
|
||||
else => |err| {
|
||||
log.err("error getting name of slave PTY errno={t}", .{err});
|
||||
return null;
|
||||
},
|
||||
}
|
||||
},
|
||||
.linux => {
|
||||
if (c.ptsname_r(self.master, &self.tty_name_buf, self.tty_name_buf.len) != 0) return null;
|
||||
const tty_name: [:0]const u8 = std.mem.sliceTo(&self.tty_name_buf, 0);
|
||||
self.tty_name = tty_name;
|
||||
return tty_name;
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Windows PTY creation and management.
|
||||
@@ -398,6 +468,13 @@ const WindowsPty = struct {
|
||||
if (result != windows.S_OK) return error.ResizeFailed;
|
||||
self.size = size;
|
||||
}
|
||||
|
||||
/// Get information about the process(es) attached to the PTY. Returns
|
||||
/// `null` if there was an error getting the information or the information
|
||||
/// is not available on a particular platform.
|
||||
pub fn getProcessInfo(_: *WindowsPty, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
@@ -419,4 +496,11 @@ test {
|
||||
ws.ws_row *= 2;
|
||||
try pty.setSize(ws);
|
||||
try testing.expectEqual(ws, try pty.getSize());
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.freebsd => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
|
||||
.linux => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/pts/")),
|
||||
.macos => try testing.expect(std.mem.startsWith(u8, pty.getProcessInfo(.tty_name).?, "/dev/")),
|
||||
else => try testing.expect(pty.getProcessInfo(.tty_name) == null),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const Pty = ptypkg.Pty;
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const PasswdEntry = internal_os.passwd.Entry;
|
||||
const windows = internal_os.windows;
|
||||
const ProcessInfo = @import("../pty.zig").ProcessInfo;
|
||||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
@@ -1226,6 +1227,14 @@ const Subprocess = struct {
|
||||
fn killCommandFlatpak(command: *FlatpakHostCommand) !void {
|
||||
try command.signal(c.SIGHUP, true);
|
||||
}
|
||||
|
||||
/// Get information about the process(es) running within the subprocess.
|
||||
/// Returns `null` if there was an error getting the information or the
|
||||
/// information is not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Subprocess, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
const pty = &(self.pty orelse return null);
|
||||
return pty.getProcessInfo(info);
|
||||
}
|
||||
};
|
||||
|
||||
/// The read thread sits in a loop doing the following pseudo code:
|
||||
@@ -1580,6 +1589,13 @@ fn execCommand(
|
||||
};
|
||||
}
|
||||
|
||||
/// Get information about the process(es) running within the backend. Returns
|
||||
/// `null` if there was an error getting the information or the information is
|
||||
/// not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Exec, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return self.subprocess.getProcessInfo(info);
|
||||
}
|
||||
|
||||
test "execCommand darwin: shell command" {
|
||||
if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const apprt = @import("../apprt.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const windows = internal_os.windows;
|
||||
const configpkg = @import("../config.zig");
|
||||
const ProcessInfo = @import("../pty.zig").ProcessInfo;
|
||||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
@@ -764,3 +765,10 @@ pub const ThreadData = struct {
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/// Get information about the process(es) attached to the backend. Returns
|
||||
/// `null` if there was an error getting the information or the information is
|
||||
/// not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Termio, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return self.backend.getProcessInfo(info);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const posix = std.posix;
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
const ProcessInfo = @import("../pty.zig").ProcessInfo;
|
||||
|
||||
// The preallocation size for the write request pool. This should be big
|
||||
// enough to satisfy most write requests. It must be a power of 2.
|
||||
@@ -100,6 +101,15 @@ pub const Backend = union(Kind) {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get information about the process(es) attached to the backend. Returns
|
||||
/// `null` if there was an error getting the information or the information
|
||||
/// is not available on a particular platform.
|
||||
pub fn getProcessInfo(self: *Backend, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return switch (self.*) {
|
||||
.exec => |*exec| exec.getProcessInfo(info),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Termio thread data. See termio.ThreadData for docs.
|
||||
|
||||
Reference in New Issue
Block a user