//go:build darwin package terminal import ( "bytes" "fmt" "os" "syscall" "unsafe" "golang.org/x/sys/unix" ) // openPTY allocates a pseudo-terminal and returns both of its ends. // // Darwin hands out the slave by name rather than by number: grant it, unlock // it, then ask for the name. TIOCPTYGNAME writes into a buffer, and x/sys/unix // exposes no helper for an ioctl of that shape — hence the one raw call below. func openPTY() (master, slave *os.File, err error) { master, err = os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0) if err != nil { return nil, nil, fmt.Errorf("terminal: opening /dev/ptmx: %w", err) } for _, step := range []struct { request uint what string }{ {unix.TIOCPTYGRANT, "granting"}, {unix.TIOCPTYUNLK, "unlocking"}, } { if err := unix.IoctlSetInt(int(master.Fd()), step.request, 0); err != nil { master.Close() return nil, nil, fmt.Errorf("terminal: %s the pseudo-terminal: %w", step.what, err) } } name, err := slaveName(master) if err != nil { master.Close() return nil, nil, err } slave, err = os.OpenFile(name, os.O_RDWR|unix.O_NOCTTY, 0) if err != nil { master.Close() return nil, nil, fmt.Errorf("terminal: opening the slave side: %w", err) } return master, slave, nil } // ptyNameLength is the size of the buffer TIOCPTYGNAME fills, as the request // itself encodes. const ptyNameLength = 128 // slaveName asks the kernel for the path of the slave device. func slaveName(master *os.File) (string, error) { var buffer [ptyNameLength]byte _, _, errno := syscall.Syscall( syscall.SYS_IOCTL, master.Fd(), uintptr(unix.TIOCPTYGNAME), uintptr(unsafe.Pointer(&buffer[0])), ) if errno != 0 { return "", fmt.Errorf("terminal: naming the pseudo-terminal: %w", errno) } // The kernel writes a C string, so the name ends at the first zero byte. if end := bytes.IndexByte(buffer[:], 0); end >= 0 { return string(buffer[:end]), nil } return string(buffer[:]), nil } // setWinsize tells the pseudo-terminal how big it is, which the kernel passes // on to the shell as SIGWINCH. func setWinsize(master *os.File, width, height int) error { return unix.IoctlSetWinsize(int(master.Fd()), unix.TIOCSWINSZ, &unix.Winsize{ Row: uint16(height), Col: uint16(width), }) } // childAttributes put the shell in a session of its own with the slave as its // controlling terminal. // // Without this the shell has no controlling terminal, and job control — Ctrl-C, // Ctrl-Z, running something in the background — does not work at all. func childAttributes() *syscall.SysProcAttr { return &syscall.SysProcAttr{Setsid: true, Setctty: true} }