Fix putting terminal into raw mode (fixes #92)

This commit is contained in:
Marcin Kulik
2015-04-16 14:05:35 +02:00
parent f2fac2cb3c
commit 0012d2c393
8 changed files with 238 additions and 4 deletions

4
Godeps/Godeps.json generated
View File

@@ -11,6 +11,10 @@
"Comment": "null-213", "Comment": "null-213",
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef" "Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
}, },
{
"ImportPath": "github.com/creack/termios/raw",
"Rev": "d60649a6c40aa68303e3d69e0423a1e7aedf4bbd"
},
{ {
"ImportPath": "github.com/docopt/docopt-go", "ImportPath": "github.com/docopt/docopt-go",
"Comment": "0.6.1-1-gc5dac53", "Comment": "0.6.1-1-gc5dac53",

View File

@@ -0,0 +1,62 @@
package raw
import (
"syscall"
"unsafe"
)
// TcSetAttr restores the terminal connected to the given file descriptor to a
// previous state.
func TcSetAttr(fd uintptr, termios *Termios) error {
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(setTermios), uintptr(unsafe.Pointer(termios))); err != 0 {
return err
}
return nil
}
// TcGetAttr retrieves the current terminal settings and returns it.
func TcGetAttr(fd uintptr) (*Termios, error) {
var termios = &Termios{}
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, getTermios, uintptr(unsafe.Pointer(termios))); err != 0 {
return nil, err
}
return termios, nil
}
// CfMakeRaw sets the flags stored in the termios structure to a state disabling
// all input and output processing, giving a ``raw I/O path''.
//
// From man cfmakeraw(3) on linux:
// termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
// termios_p->c_oflag &= ~OPOST;
// termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
// termios_p->c_cflag &= ~(CSIZE | PARENB);
// termios_p->c_cflag |= CS8;
//
func CfMakeRaw(termios *Termios) {
termios.Iflag &^= (syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON)
termios.Oflag &^= syscall.OPOST
termios.Lflag &^= (syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN)
termios.Cflag &^= (syscall.CSIZE | syscall.PARENB)
termios.Cflag |= syscall.CS8
termios.Cc[syscall.VMIN] = 1
termios.Cc[syscall.VTIME] = 0
}
// MakeRaw sets the flags stored in the termios structure for the given terminal fd
// to a state disabling all input and output processing, giving a ``raw I/O path''.
// It returns the current terminal's termios struct to allow to revert with TcSetAttr
func MakeRaw(fd uintptr) (*Termios, error) {
old, err := TcGetAttr(fd)
if err != nil {
return nil, err
}
new := *old
CfMakeRaw(&new)
if err := TcSetAttr(fd, &new); err != nil {
return nil, err
}
return old, nil
}

View File

@@ -0,0 +1,115 @@
package raw
import (
"syscall"
"testing"
"time"
"github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/kr/pty"
)
func TestCfMakeRaw(t *testing.T) {
termios := &Termios{
Iflag: 0x2b02,
Oflag: 0x1,
Lflag: 0x5c3,
Cflag: 0x4b00,
Cc: [20]byte{
0x4, 0xff, 0xff, 0xff,
0x17, 0xff, 0x12, 0xff,
0x3, 0x1c, 0x1a, 0x19,
0x11, 0x13, 0x16, 0xf,
0xff, 0xff, 0x14, 0xff,
},
Ispeed: 0x2580, // (9600)
Ospeed: 0x2580, // (9600)
}
CfMakeRaw(termios)
want := Termios{
Iflag: 0x2800,
Oflag: 0x0,
Lflag: 0x43,
Cflag: 0x4b00,
Cc: [20]byte{
0x4, 0xff, 0xff, 0xff,
0x17, 0xff, 0x12, 0xff,
0x3, 0x1c, 0x1a, 0x19,
0x11, 0x13, 0x16, 0xf,
0x1, 0x0, 0x14, 0xff,
},
Ispeed: 0x2580,
Ospeed: 0x2580,
}
if got := *termios; want != got {
t.Fatalf("Unexpected Raw termios.\nGot:\t %#v\nWant:\t %#v\n", got, want)
}
}
func TestMakeRaw(t *testing.T) {
// Create a PTY pair to play with
master, slave, err := pty.Open()
if err != nil {
t.Fatal(err)
}
defer master.Close()
defer slave.Close()
// Apply the raw mode on the slave
slaveTermios, err := MakeRaw(slave.Fd())
if err != nil {
t.Fatal(err)
}
// Make a copy of the original termios and manually apply cfmakeraw
slaveTermiosOrig := *slaveTermios
CfMakeRaw(&slaveTermiosOrig)
// Retrieve the new termios on the slave after NakeRaw
slaveTermiosRaw, err := TcGetAttr(slave.Fd())
if err != nil {
t.Fatal(err)
}
// Make sure the new termios are the one we want
if slaveTermiosOrig != *slaveTermiosRaw {
t.Fatalf("Unepexpected termios.\nGot:\t %#v\nWant:\t %#v\n", *slaveTermiosRaw, slaveTermiosOrig)
}
// Simple read/write test on the master/slave pair
want := "hello world!"
go master.WriteString(want)
var (
buf = make([]byte, 64)
c = make(chan struct{})
)
// Without raw mode, as there is no \n, read will block forever
go func() {
defer close(c)
n, err := slave.Read(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
}()
go func() {
time.Sleep(2 * time.Second)
buf = []byte("timeout")
close(c)
}()
<-c
if got := string(buf); got != want {
t.Fatalf("Unexpected result.\nGot: %s\nWant: %s\n", got, want)
}
}
func BenchmarkCfMakeRaw(b *testing.B) {
t := &Termios{}
for i := 0; i < b.N; i++ {
CfMakeRaw(t)
if t.Cc[syscall.VMIN] != 1 {
b.Fatalf("err")
}
}
}

View File

@@ -0,0 +1,17 @@
// +build linux freebsd
package raw
// Termios holds the TTY attributes. See man termios(4).
// Tested on linux386, linux/arm, linux/amd64,
// freebsd/386, freebsd/arm, freebsd/amd64.
// See tremios_64.go for darwin.
type Termios struct {
Iflag uint32
Oflag uint32
Cflag uint32
Lflag uint32
Cc [20]byte
Ispeed uint32
Ospeed uint32
}

View File

@@ -0,0 +1,15 @@
// +build darwin
package raw
// Termios holds the TTY attributes. See man termios(4).
// Tested on darwin/386, darwin/amd64. See termios_32.go for others.
type Termios struct {
Iflag uint64
Oflag uint64
Cflag uint64
Lflag uint64
Cc [20]byte
Ispeed uint64
Ospeed uint64
}

View File

@@ -0,0 +1,10 @@
// +build darwin freebsd
package raw
import "syscall"
const (
getTermios = syscall.TIOCGETA
setTermios = syscall.TIOCSETA
)

View File

@@ -0,0 +1,10 @@
// +build linux
package raw
import "syscall"
const (
getTermios = syscall.TCGETS
setTermios = syscall.TCSETS
)

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/asciinema/asciinema/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal" "github.com/asciinema/asciinema/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal"
"github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/creack/termios/raw"
"github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/kr/pty" "github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/kr/pty"
"github.com/asciinema/asciinema/ptyx" "github.com/asciinema/asciinema/ptyx"
"github.com/asciinema/asciinema/util" "github.com/asciinema/asciinema/util"
@@ -55,13 +56,13 @@ func (p *Pty) Record(command string, stdoutCopy io.Writer) error {
defer close(signals) defer close(signals)
// put stdin in raw mode (if it's a tty) // put stdin in raw mode (if it's a tty)
fd := int(p.Stdin.Fd()) fd := p.Stdin.Fd()
if terminal.IsTerminal(fd) { if terminal.IsTerminal(int(fd)) {
oldState, err := terminal.MakeRaw(fd) oldState, err := raw.MakeRaw(fd)
if err != nil { if err != nil {
return err return err
} }
defer terminal.Restore(fd, oldState) defer raw.TcSetAttr(fd, oldState)
} }
// do initial resize // do initial resize