2
\$\begingroup\$

I'm rewriting GNU's coreutils in Go in order to learn Go better. I've implemented ttyname(3), and I'm looking for a general critique. I can't think of anything in particular I think I did poorly on. (Meaning nothing stands out to me -- I'm sure I messed something up.)

I do return a pointer to a string instead of a string, so maybe that's bad. If you're looking for the source of ReadDir(), it can be found here, and clen() can be found here. Sorry about the links.

package gnulib

import (
    "errors"
    "io"
    "os"
    "path"
    "syscall"
    "unsafe"
)

const (
    dev = "/dev"
    proc = "/proc/self/fd/0"
)

var (
    NotFound   = errors.New("device not found")
    NotTty     = errors.New("not a tty device")
    searchDevs = []string{
        "/dev/pts/",
        "/dev/console",
        "/dev/wscons",
        "/dev/vt/",
        "/dev/term/",
        "/dev/zcons/",
    }
    Stat = new(syscall.Stat_t)
)

// recursively walk through the named directory `dir` until the correct device
// is found.
// stdin, stdout, stdin, and /dev/fd are all skipped
// entries in []searchDevs are searched first
func checkDirs(dir string) (*string, error) {
    var (
        rs      *string
        nameBuf = make([]byte, 256)
    )

    fi, err := os.Open(dir)
    if err != nil {
        return nil, err
    }
    defer fi.Close()

    dirBuf := make(DirentBuf)
    err = ReadDir(int(fi.Fd()), -1, &dirBuf)
    if err != nil && err != io.EOF {
        return nil, err
    }

    for _, v := range dirBuf {
        // quickly skip most entries
        if v.Ino != Stat.Ino {
            continue
        }

        _ = copy(nameBuf, int8ToByte(v.Name[:]))
        name := path.Join(dir, string(nameBuf[:clen(nameBuf)]))

        // Directories to skip
        if name == "/dev/stderr" ||
            name == "/dev/stdin" ||
            name == "/dev/stdout" ||
            len(name) >= 8 &&
                name[0:8] == "/dev/fd/" {
            continue
        }

        // We have to stat the file to determine its Rdev
        fstat := new(syscall.Stat_t)
        err = syscall.Stat(name, fstat)
        if err != nil {
            continue
        }

        // file mode sans permission bits
        fmode := os.FileMode(fstat.Mode)
        if fmode.IsDir() {
            rs, err = checkDirs(name)
            if err != nil {
                continue
            }

            return rs, nil
        }

        if fmode&os.ModeCharDevice == 0 &&
            fstat.Ino == Stat.Ino &&
            fstat.Rdev == Stat.Rdev {
            return &name, nil
        }

    }

    return nil, NotFound
}

// quick istty check
func isTty(fd uintptr) bool {
    var termios syscall.Termios

    _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd,
        uintptr(syscall.TCGETS),
        uintptr(unsafe.Pointer(&termios)),
        0,
        0,
        0)
    return err == 0
}

// Returns a pointer to a string from a uintptr describing a file descriptor
func TtyName(fd uintptr) (*string, error) {
    var name *string

    // Does `fd` even describe a terminal? ;)
    if !isTty(fd) {
        return nil, NotTty
    }

    // Gather inode and rdev info about fd
    err := syscall.Fstat(int(fd), Stat)
    if err != nil {
        return nil, err
    }

    // Needs to be a character device
    if os.FileMode(Stat.Mode)&os.ModeCharDevice != 0 {
        return nil, NotTty
    }

    // strace of tty stats the return of readlink(/proc/self/fd)
    // let's do that instead, and fall back on searching /dev/
    if ret, _ := os.Readlink(proc); ret != "" {
        fstat := new(syscall.Stat_t)
        _ = syscall.Stat(ret, fstat)

        if os.FileMode(fstat.Mode)&os.ModeCharDevice == 0 &&
            fstat.Ino == Stat.Ino &&
            fstat.Rdev == Stat.Rdev {
            return &ret, nil
        }
    }

    // Loop over most likely directories second
    for _, v := range searchDevs {
        name, _ = checkDirs(v)
        if name != nil {
            return name, nil
        }
    }

    // If we can't find it above, do full scan of /dev/
    if name == nil {
        name, _ = checkDirs(dev)
        return name, nil
    }

    return nil, NotFound
}
\$\endgroup\$
  • \$\begingroup\$ Massively OS specific, probably better to wrap libc's ttyname via cgo. And don't directly muck around in syscall when there alternatives (e.g. os.Stat). \$\endgroup\$ – Dave C Apr 12 '15 at 16:58
  • \$\begingroup\$ @DaveC yup, it is. I've since written cross platform versions. I used syscall because I needed to inode and dev #s, and it's the same as stat.Sys()(*syscall.stat_t) \$\endgroup\$ – Eric Lagergren Apr 12 '15 at 17:02
  • \$\begingroup\$ Unrelated but... have you considered open sourcing your project? It sounds awesome! \$\endgroup\$ – TomOnTime May 3 '15 at 21:22
2
\$\begingroup\$

There is no reason to use sys call since the FileInfo.Sys() returned by os.Stat will give you the syscall.Stat_t data.

Here is an alternate implementation that seems to be much simpler. I do not believe you have to special case so many files. For example, /dev/stdin on linux is a symbolic link so it will not match when checking the raw device number.

The init function is not really needed, as it mentions below, it is just so you can use os.IsNotExist.

// Package ttyname finds the tty name associate with a *os.File or numeric file
// descriptor on Unix(TM) like systems.  Examples:
//
//  name, err := ttyname.File(os.Stdin)
//  name, err := ttyname.Fileno(0)
package ttyname

import (
    "errors"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "syscall"
)

// ttydirs is the list of possible directories the tty device is found in.
// It may be updating for other operating systems.
var ttydirs = []string{
    "/dev/pty", // Linux
    "/dev",     // Unix, Darwin, ...
}

var (
    ErrNotTTY   = errors.New("not a tty")
    ErrNotFound = errors.New("tty device not found")
)

func init() {
    // This is a hack to make os.IsNotExist(ErrNotFound) return true.
    // It isn't actually necessary.
    _, err := os.Stat("/\001not\001exist\001")
    if pe, ok := err.(*os.PathError); ok {
        if _, ok := pe.Err.(syscall.Errno); ok {
            ErrNotFound = &os.SyscallError{
                Syscall: "tty",
                Err:     pe.Err,
            }
        }
    }
}

// Fileno either returns the device name of the tty attached to the provided
// file descriptor or an error.  If fileno is not attached to a tty, ErrNotTTY
// is returned.
func Fileno(fileno int) (string, error) {
    return File(os.NewFile(uintptr(fileno), "tty"))
}

// File either returns the device name of the tty attached to the provided
// file descriptor or an error.  If tty is not attached to a tty, ErrNotTTY
// is returned.
func File(tty *os.File) (string, error) {
    fi, err := tty.Stat()
    if err != nil {
        return "", err
    }
    if fi.Mode()&(os.ModeDevice|os.ModeCharDevice) != (os.ModeDevice | os.ModeCharDevice) {
        return "", ErrNotTTY
    }
    sys, ok := fi.Sys().(*syscall.Stat_t)
    if !ok {
        return "", fmt.Errorf("unsupported sys stat type %T", sys)
    }
    for _, dir := range ttydirs {
        name, _ := searchDir(dir, sys)
        if name != "" {
            return filepath.Join(dir, name), nil
        }
    }
    return "", ErrNotFound
}

// searchDir searches the directory dir for a file that has the same raw device
// number as in stat.  It either returns the name of the file found or an error.
// searchDir does not descend into other directories.
func searchDir(dir string, stat *syscall.Stat_t) (string, error) {
    fd, err := os.Open(dir)
    if err != nil {
        if os.IsNotExist(err) {
            return "", nil
        }
        return "", err
    }
    defer fd.Close()
    for {
        fis, err := fd.Readdir(256)
        for _, fi := range fis {
            s, ok := fi.Sys().(*syscall.Stat_t)
            if ok && stat.Rdev == s.Rdev {
                return fi.Name(), nil
            }
        }
        switch err {
        case nil:
        case io.EOF:
            return "", nil
        default:
            return "", err
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

ttyname is implemented in the tudurom/ttyname package.

The implementation is short and sweet (it supports several platforms, this is for Linux):

package ttyname

import "os"

const fd0 = "/proc/self/fd/0"

// TTY prints the file name of the terminal connected to standard input
func TTY() (string, error) {
    dest, err := os.Readlink(fd0)
    if err != nil {
        return "", err
    }
    return dest, nil
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.