package main

import (

"os"
"time"
"io"
"os/exec"
"syscall"
"unsafe"
"fmt"

)

/* forkpty == openpty + fork

parent: close slave
child: close master

openpty ==

master = getpt
granpt(master)
unlockpt(master)
open as file ptsname(master)
  tcsetattr (slave, TCSAFLUSH, termp);
  ioctl (slave, TIOCSWINSZ, winp);

getpt

open /dev/ptmx, return fd (linux specific)

grantpt

call ptsname (get the filename of the slave)
chown/chgrp/chmod the filename to us

unlockpt

ioctl(master, TIOCSPTLCK, 0)

open slave

if 'login terminal'
  ioctl(slave, TIOCSCTTY, NULL) // set this procses as controlling terminal
dup stdin/stdout/stderr to slave

*/

func getpt() (file *os.File, err error) {

file, err = os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
  return nil, err
}
return file, nil

} /* getpt */

func ptsname(file *os.File) (name string, err error) {

/* On linux, this calls ioctl(fd, TIOCGPTN, ...) */
var num int

/* Get the /dev/pts number */
err = ioctl(file, syscall.TIOCGPTN, &num)
if err != nil && err.Error() != "errno 0" {
  return "", err
}
return fmt.Sprintf("/dev/pts/%d", num), nil

}

func grantpt(file *os.File) (err error) {

slave_name, err := ptsname(file)
if err != nil { return err }
err = os.Chown(slave_name, os.Getuid(), os.Getgid())
if err != nil { return err }
err = os.Chmod(slave_name, 0600)
if err != nil { return err }
return nil

}

func unlockpt(file *os.File) (err error) {

var val = 0
err = ioctl(file, syscall.TIOCSPTLCK, &val)

if err != nil && err.Error() != "errno 0" { return err }
return nil

}

/* Borrowed with modifications from github.com/kr/pty/pty_linux.go; MIT license */ func ioctl(file *os.File, command uint, data *int) (err syscall.Errno) {

_, _, err = syscall.Syscall(syscall.SYS_IOCTL, uintptr(file.Fd()),
                             uintptr(command), uintptr(unsafe.Pointer(data)))
if err != 0 {
  return err
}
return syscall.Errno(0)

} /* ioctl */

func openpty() (master *os.File, slave *os.File, err error) {

master, err = getpt()
if err != nil { return nil, nil, err }
if err = grantpt(master); err != nil { return nil, nil, err }
if err = unlockpt(master); err != nil { return nil, nil, err }

slave_name, err := ptsname(master)
if err != nil { return nil, nil, err }
slave, err = os.OpenFile(slave_name, os.O_RDWR, 0)

return master, slave, nil

} /* openpty */

func dup(file *os.File, name string) (newfile *os.File, err error) {

fd, err := syscall.Dup(int(file.Fd()))
if err != nil { return nil, err }

return os.NewFile(uintptr(fd), "<stdin>"), nil

}

func forkpty(name string, argv []string, attr *os.ProcAttr) (master *os.File, command *exec.Cmd, err error) {

master, slave, err := openpty()
if err != nil { return nil, nil, err }

/* dup it up. */

fd := [3]*os.File{slave, slave, slave}
attr.Files = fd[:]

command = new(exec.Cmd)
//command.Path = name
//command.Args = argv[:]
command.Stdin, err = dup(slave, "<slave stdin>")
command.Stdout, err = dup(slave, "<slave stdout>")
command.Stderr, err = dup(slave, "<slave stderr>")
//command.Stdout = slave
//command.Stderr = slave
command.Process, err = os.StartProcess(name, argv, attr)
slave.Close()
if err != nil { return nil, nil, err }

/* Now in the parent */
command.Stdin, err = dup(master, "<stdin>")
command.Stdout, err = dup(master, "<stdout>")
command.Stderr, err = dup(master, "<stderr>")
//command.Stdin = master
//command.Stdout = master
//command.Stderr = master
//master.Close()

if err != nil { return nil, nil, err }
return master, command, nil

}

func main() {

master, command, err := forkpty("/bin/bash", []string{"/bin/bash", "-li"}, new(os.ProcAttr))

if err != nil { fmt.Printf("forkpty: %v\n", err); return }
fmt.Printf("%T/%v %s\n", master, master, master.Name())

go func() {
  for { 
    data := make([]byte, 1024)
    _, err := command.Stdout.(io.Reader).Read(data)
    if err != nil { return }
    //fmt.Printf("Read: %d '%#v'\n", size, fmt.Sprintf("%.*s", size, data))
    os.Stdout.Write(data)
    //time.Sleep(500 * time.Millisecond)
  }
}()

time.Sleep(500 * time.Millisecond)
_, err = command.Stdin.(*os.File).WriteString("echo hello world\n")
if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
time.Sleep(500 * time.Millisecond)
_, err = command.Stdin.(*os.File).WriteString("tty\n")
if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
time.Sleep(500 * time.Millisecond)
//_, err = command.Stdin.(*os.File).WriteString("exit\n")
//if err != nil { fmt.Printf("Fprintf: %v\n", err); return }
master.Close()

/* It would be nice if this actually closed stdin for the subcommand */
command.Stdin.(*os.File).Close()
fmt.Printf("Wait: %v\n", command.Wait())

}